From f3595eebea35745886cd2fd6124694d329657c25 Mon Sep 17 00:00:00 2001 From: Jambl3r <54366245+jambl3r@users.noreply.github.com> Date: Thu, 15 Jan 2026 20:30:14 +0000 Subject: [PATCH 01/48] #1539 fix: converted strings to en-US --- base/src/main/res/values/strings.xml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index dceb055592..72a2f782f4 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -50,7 +50,7 @@ Key Mapper log What\'s New - You can either use a system ringtone or select a custom sound file.\n\nThe custom sound file will be copied to Key Mapper\'s private data folder, which means your actions will still work even if the file is moved or deleted. It will also be backed up with your key maps in the zip folder. + You can either use a system ringtone or select a custom sound file.\n\nThe custom sound file will be copied to Key Mapper\'s private data folder, which means your actions will still work even if the file is moved or deleted. It will also be included in backups with your key maps in the zip file. You can delete saved sound files in the settings. Can\'t find any paired devices. Is Bluetooth turned on? @@ -82,7 +82,7 @@ Trigger device not connected! Migrate this screen off trigger - Disable battery optimisation. + Disable battery optimization. Your key maps are paused! Unpause The accessibility service needs to be turned on for your key maps to work! @@ -277,7 +277,7 @@ Connected to %s WiFi Disconnected from %s WiFi Connected to any WiFi - Disconnected to no WiFi + Disconnected from any WiFi Input method is chosen %s is chosen @@ -291,8 +291,8 @@ Device is locked Device is unlocked - Lockscreen is showing - Lockscreen is not showing + Lock screen is showing + Lock screen is not showing In phone call Not in phone call @@ -450,18 +450,18 @@ Android doesn\'t allow apps to get a list of connected (not paired) Bluetooth devices. Apps can only detect when they are connected and disconnected. So if your Bluetooth device is already connected to your device when the accessibility service starts, you will have to reconnect it for the app to know it is connected. Automatic backup Change location or turn off automatic back up? - If you have any other screen lock chosen, such as PIN or Pattern then you don\'t have to worry. But if you have a Password screen lock you will *NOT* be able to unlock your phone if you use the Key Mapper Input Method because it doesn\'t have a GUI. You can grant Key Mapper WRITE_SECURE_SETTINGS permission so it can show a notification to switch to and from the keyboard. There is a guide on how to do this if you tap the question mark at the bottom of the screen. + If you use a PIN or Pattern to unlock your device, then you don\'t have to worry. But if you have a Password screen lock, you will *NOT* be able to unlock your phone if you use the Key Mapper Input Method because it doesn\'t have a GUI. You can grant Key Mapper WRITE_SECURE_SETTINGS permission so it can show a notification to switch to and from the keyboard. There is a guide on how to do this if you tap the question mark at the bottom of the screen. Select the input method for actions that require one. You can change this later by tapping \"Select keyboard for actions\" in the bottom menu of the home screen. No external devices connected. - Disable battery optimisation + Disable battery optimization You MUST read this all otherwise you will get frustrated in the future!\n\nTapping \"fix partially\" might prevent Android from stopping the app while it is in the background.\n\nThis is NOT ENOUGH. Your OEM\'s skin such as MIUI or Samsung Experience may have other app killing features so you MUST turn them off for Key Mapper as well by following the online guide at dontkillmyapp.com. Restart the accessibility service by turning it off and on. Key Mapper was interrupted - Key Mapper tried to run in the background but was stopped by the system.\nThis can happen if you have battery or memory optimisation turned on.\n\nTo fix this, you can try following an online guide. You should also restart the service when you\'re done. + Key Mapper tried to run in the background but was stopped by the system.\nThis can happen if you have battery or memory optimization turned on.\n\nTo fix this, you can try following an online guide. You should also restart the service when you\'re done. Proceed Ignore @@ -503,7 +503,7 @@ Use Expert Mode Change Fix partially - Ok + OK Restart Never show again Apply @@ -824,7 +824,7 @@ Failed perform global action %s! This action needs setting up - Battery optimisation settings not found! If it exists, open it manually. + Battery optimization settings not found! If it exists, open it manually. Extra (%s) not found! You can\'t have duplicate constraints! @@ -833,11 +833,11 @@ Empty JSON file! File access denied! %s Unknown IO error! - Cancelled! + Canceled! Invalid number! Must be at least %s! Must be at most %s! - Battery optimisation is turned on! Turn this off because this can randomly stop Key Mapper from working. + Battery optimization is turned on! Turn this off because this can randomly stop Key Mapper from working. Denied notification access permission! Invalid! Denied permission to start phone calls! @@ -1379,7 +1379,7 @@ Loading… Purchased! Retry fetching price - Purchase cancelled. + Purchase canceled. This requires a paid feature that can only be bought by downloading Key Mapper from Google Play. Network error encountered. Do you have an internet connection? This product was not found. @@ -1394,7 +1394,7 @@ You must purchase the floating buttons feature! Tap on the key map and then purchase it by clicking on the shop. contact@keymapper.club Key Mapper Pro query - Please fill the following information so I can help you.\n\n1. Device model:\n2. Android version:\n3. Key maps (make a back up in the home screen menu):\n4. Screenshot of Key Mapper home screen:\n5. Describe the problem you are having: + Please fill the following information so I can help you.\n\n1. Device model:\n2. Android version:\n3. Key maps (make a backup in the home screen menu):\n4. Screenshot of Key Mapper home screen:\n5. Describe the problem you are having: Thank you for supporting the app! Your purchase was successful. As a paying user of Key\u00A0Mapper you will receive priority support to help you use the app. There is now a button in the shop to contact the developer. 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 0783483ab8678a1e26fa22e26890adf61f565cf6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 21:23:13 +0100 Subject: [PATCH 02/48] style: reformat --- evdev/src/main/rust/evdev/.cargo/config.toml | 1 + evdev/src/main/rust/evdev_manager/.cargo/config.toml | 1 + .../core/src/android/keylayout/generic_key_layout.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/evdev/src/main/rust/evdev/.cargo/config.toml b/evdev/src/main/rust/evdev/.cargo/config.toml index f2084c6b7f..7c6f86610a 100644 --- a/evdev/src/main/rust/evdev/.cargo/config.toml +++ b/evdev/src/main/rust/evdev/.cargo/config.toml @@ -13,3 +13,4 @@ rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] + diff --git a/evdev/src/main/rust/evdev_manager/.cargo/config.toml b/evdev/src/main/rust/evdev_manager/.cargo/config.toml index f2084c6b7f..7c6f86610a 100644 --- a/evdev/src/main/rust/evdev_manager/.cargo/config.toml +++ b/evdev/src/main/rust/evdev_manager/.cargo/config.toml @@ -13,3 +13,4 @@ rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] + diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs index ac7f35eb89..b454076831 100644 --- a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs +++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs @@ -286,3 +286,4 @@ axis 0x0a LTRIGGER axis 0x10 HAT_X axis 0x11 HAT_Y "#; + From 9728352b9ab3c69475159fb980b981eae177e6b3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 21:23:32 +0100 Subject: [PATCH 03/48] chore: bump version to 4.0.0 beta 7 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index c4fdc8c42a..02a56b5636 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.0.0-beta.06 -VERSION_CODE=226 +VERSION_NAME=4.0.0-beta.07 +VERSION_CODE=228 From 162772578255ddddd9e132320a4c7b2320730961 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 21:28:25 +0100 Subject: [PATCH 04/48] #1970 fix: dynamically build the key code list so key codes in new Android releases are automatically included. --- CHANGELOG.md | 8 + .../system/inputevents/KeyEventUtils.kt | 328 +----------------- 2 files changed, 19 insertions(+), 317 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db78ba37e9..a44fc81893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [4.0.0 Beta 7](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.07) + +#### TO BE RELEASED + +## Added + +- #1970 dynamically build the key code list so key codes in new Android releases are automatically included. + ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) #### 4 January 2026 diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt index 9c3a04080c..613635c6e0 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt @@ -4,323 +4,11 @@ import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.withFlag object KeyEventUtils { - private val KEYCODES: IntArray = intArrayOf( - KeyEvent.KEYCODE_SOFT_LEFT, - KeyEvent.KEYCODE_SOFT_RIGHT, - KeyEvent.KEYCODE_HOME, - KeyEvent.KEYCODE_BACK, - KeyEvent.KEYCODE_CALL, - KeyEvent.KEYCODE_ENDCALL, - KeyEvent.KEYCODE_0, - KeyEvent.KEYCODE_1, - KeyEvent.KEYCODE_2, - KeyEvent.KEYCODE_3, - KeyEvent.KEYCODE_4, - KeyEvent.KEYCODE_5, - KeyEvent.KEYCODE_6, - KeyEvent.KEYCODE_7, - KeyEvent.KEYCODE_8, - KeyEvent.KEYCODE_9, - KeyEvent.KEYCODE_STAR, - KeyEvent.KEYCODE_POUND, - KeyEvent.KEYCODE_DPAD_UP, - KeyEvent.KEYCODE_DPAD_DOWN, - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_VOLUME_UP, - KeyEvent.KEYCODE_VOLUME_DOWN, - KeyEvent.KEYCODE_POWER, - KeyEvent.KEYCODE_CAMERA, - KeyEvent.KEYCODE_CLEAR, - KeyEvent.KEYCODE_A, - KeyEvent.KEYCODE_B, - KeyEvent.KEYCODE_C, - KeyEvent.KEYCODE_D, - KeyEvent.KEYCODE_E, - KeyEvent.KEYCODE_F, - KeyEvent.KEYCODE_G, - KeyEvent.KEYCODE_H, - KeyEvent.KEYCODE_I, - KeyEvent.KEYCODE_J, - KeyEvent.KEYCODE_K, - KeyEvent.KEYCODE_L, - KeyEvent.KEYCODE_M, - KeyEvent.KEYCODE_N, - KeyEvent.KEYCODE_O, - KeyEvent.KEYCODE_P, - KeyEvent.KEYCODE_Q, - KeyEvent.KEYCODE_R, - KeyEvent.KEYCODE_S, - KeyEvent.KEYCODE_T, - KeyEvent.KEYCODE_U, - KeyEvent.KEYCODE_V, - KeyEvent.KEYCODE_W, - KeyEvent.KEYCODE_X, - KeyEvent.KEYCODE_Y, - KeyEvent.KEYCODE_Z, - KeyEvent.KEYCODE_COMMA, - KeyEvent.KEYCODE_PERIOD, - KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_ALT_RIGHT, - KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_SHIFT_RIGHT, - KeyEvent.KEYCODE_TAB, - KeyEvent.KEYCODE_SPACE, - KeyEvent.KEYCODE_SYM, - KeyEvent.KEYCODE_EXPLORER, - KeyEvent.KEYCODE_ENVELOPE, - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_FORWARD_DEL, - KeyEvent.KEYCODE_DEL, - KeyEvent.KEYCODE_GRAVE, - KeyEvent.KEYCODE_MINUS, - KeyEvent.KEYCODE_EQUALS, - KeyEvent.KEYCODE_LEFT_BRACKET, - KeyEvent.KEYCODE_RIGHT_BRACKET, - KeyEvent.KEYCODE_BACKSLASH, - KeyEvent.KEYCODE_SEMICOLON, - KeyEvent.KEYCODE_APOSTROPHE, - KeyEvent.KEYCODE_SLASH, - KeyEvent.KEYCODE_AT, - KeyEvent.KEYCODE_HEADSETHOOK, - KeyEvent.KEYCODE_FOCUS, - KeyEvent.KEYCODE_PLUS, - KeyEvent.KEYCODE_MENU, - KeyEvent.KEYCODE_NOTIFICATION, - KeyEvent.KEYCODE_SEARCH, - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, - KeyEvent.KEYCODE_MEDIA_STOP, - KeyEvent.KEYCODE_MEDIA_NEXT, - KeyEvent.KEYCODE_MEDIA_PREVIOUS, - KeyEvent.KEYCODE_MEDIA_REWIND, - KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, - KeyEvent.KEYCODE_PAGE_UP, - KeyEvent.KEYCODE_PAGE_DOWN, - KeyEvent.KEYCODE_PICTSYMBOLS, - KeyEvent.KEYCODE_SWITCH_CHARSET, - KeyEvent.KEYCODE_BUTTON_A, - KeyEvent.KEYCODE_BUTTON_B, - KeyEvent.KEYCODE_BUTTON_C, - KeyEvent.KEYCODE_BUTTON_X, - KeyEvent.KEYCODE_BUTTON_Y, - KeyEvent.KEYCODE_BUTTON_Z, - KeyEvent.KEYCODE_BUTTON_L1, - KeyEvent.KEYCODE_BUTTON_R1, - KeyEvent.KEYCODE_BUTTON_L2, - KeyEvent.KEYCODE_BUTTON_R2, - KeyEvent.KEYCODE_BUTTON_THUMBL, - KeyEvent.KEYCODE_BUTTON_THUMBR, - KeyEvent.KEYCODE_BUTTON_START, - KeyEvent.KEYCODE_BUTTON_SELECT, - KeyEvent.KEYCODE_BUTTON_MODE, - KeyEvent.KEYCODE_ESCAPE, - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_CTRL_RIGHT, - KeyEvent.KEYCODE_CAPS_LOCK, - KeyEvent.KEYCODE_SCROLL_LOCK, - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_META_RIGHT, - KeyEvent.KEYCODE_FUNCTION, - KeyEvent.KEYCODE_SYSRQ, - KeyEvent.KEYCODE_BREAK, - KeyEvent.KEYCODE_MOVE_HOME, - KeyEvent.KEYCODE_MOVE_END, - KeyEvent.KEYCODE_INSERT, - KeyEvent.KEYCODE_FORWARD, - KeyEvent.KEYCODE_MEDIA_PLAY, - KeyEvent.KEYCODE_MEDIA_PAUSE, - KeyEvent.KEYCODE_MEDIA_CLOSE, - KeyEvent.KEYCODE_MEDIA_EJECT, - KeyEvent.KEYCODE_MEDIA_RECORD, - KeyEvent.KEYCODE_F1, - KeyEvent.KEYCODE_F2, - KeyEvent.KEYCODE_F3, - KeyEvent.KEYCODE_F4, - KeyEvent.KEYCODE_F5, - KeyEvent.KEYCODE_F6, - KeyEvent.KEYCODE_F7, - KeyEvent.KEYCODE_F8, - KeyEvent.KEYCODE_F9, - KeyEvent.KEYCODE_F10, - KeyEvent.KEYCODE_F11, - KeyEvent.KEYCODE_F12, - KeyEvent.KEYCODE_NUM, - KeyEvent.KEYCODE_NUM_LOCK, - KeyEvent.KEYCODE_NUMPAD_0, - KeyEvent.KEYCODE_NUMPAD_1, - KeyEvent.KEYCODE_NUMPAD_2, - KeyEvent.KEYCODE_NUMPAD_3, - KeyEvent.KEYCODE_NUMPAD_4, - KeyEvent.KEYCODE_NUMPAD_5, - KeyEvent.KEYCODE_NUMPAD_6, - KeyEvent.KEYCODE_NUMPAD_7, - KeyEvent.KEYCODE_NUMPAD_8, - KeyEvent.KEYCODE_NUMPAD_9, - KeyEvent.KEYCODE_NUMPAD_DIVIDE, - KeyEvent.KEYCODE_NUMPAD_MULTIPLY, - KeyEvent.KEYCODE_NUMPAD_SUBTRACT, - KeyEvent.KEYCODE_NUMPAD_ADD, - KeyEvent.KEYCODE_NUMPAD_DOT, - KeyEvent.KEYCODE_NUMPAD_COMMA, - KeyEvent.KEYCODE_NUMPAD_ENTER, - KeyEvent.KEYCODE_NUMPAD_EQUALS, - KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN, - KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN, - KeyEvent.KEYCODE_MUTE, - KeyEvent.KEYCODE_VOLUME_MUTE, - KeyEvent.KEYCODE_INFO, - KeyEvent.KEYCODE_CHANNEL_UP, - KeyEvent.KEYCODE_CHANNEL_DOWN, - KeyEvent.KEYCODE_ZOOM_IN, - KeyEvent.KEYCODE_ZOOM_OUT, - KeyEvent.KEYCODE_TV, - KeyEvent.KEYCODE_WINDOW, - KeyEvent.KEYCODE_GUIDE, - KeyEvent.KEYCODE_DVR, - KeyEvent.KEYCODE_BOOKMARK, - KeyEvent.KEYCODE_CAPTIONS, - KeyEvent.KEYCODE_SETTINGS, - KeyEvent.KEYCODE_TV_POWER, - KeyEvent.KEYCODE_TV_INPUT, - KeyEvent.KEYCODE_STB_POWER, - KeyEvent.KEYCODE_STB_INPUT, - KeyEvent.KEYCODE_AVR_POWER, - KeyEvent.KEYCODE_AVR_INPUT, - KeyEvent.KEYCODE_PROG_RED, - KeyEvent.KEYCODE_PROG_GREEN, - KeyEvent.KEYCODE_PROG_YELLOW, - KeyEvent.KEYCODE_PROG_BLUE, - KeyEvent.KEYCODE_APP_SWITCH, - KeyEvent.KEYCODE_BUTTON_1, - KeyEvent.KEYCODE_BUTTON_2, - KeyEvent.KEYCODE_BUTTON_3, - KeyEvent.KEYCODE_BUTTON_4, - KeyEvent.KEYCODE_BUTTON_5, - KeyEvent.KEYCODE_BUTTON_6, - KeyEvent.KEYCODE_BUTTON_7, - KeyEvent.KEYCODE_BUTTON_8, - KeyEvent.KEYCODE_BUTTON_9, - KeyEvent.KEYCODE_BUTTON_10, - KeyEvent.KEYCODE_BUTTON_11, - KeyEvent.KEYCODE_BUTTON_12, - KeyEvent.KEYCODE_BUTTON_13, - KeyEvent.KEYCODE_BUTTON_14, - KeyEvent.KEYCODE_BUTTON_15, - KeyEvent.KEYCODE_BUTTON_16, - KeyEvent.KEYCODE_LANGUAGE_SWITCH, - KeyEvent.KEYCODE_MANNER_MODE, - KeyEvent.KEYCODE_3D_MODE, - KeyEvent.KEYCODE_CONTACTS, - KeyEvent.KEYCODE_CALENDAR, - KeyEvent.KEYCODE_MUSIC, - KeyEvent.KEYCODE_CALCULATOR, - KeyEvent.KEYCODE_ZENKAKU_HANKAKU, - KeyEvent.KEYCODE_EISU, - KeyEvent.KEYCODE_MUHENKAN, - KeyEvent.KEYCODE_HENKAN, - KeyEvent.KEYCODE_KATAKANA_HIRAGANA, - KeyEvent.KEYCODE_YEN, - KeyEvent.KEYCODE_RO, - KeyEvent.KEYCODE_KANA, - KeyEvent.KEYCODE_ASSIST, - KeyEvent.KEYCODE_BRIGHTNESS_DOWN, - KeyEvent.KEYCODE_BRIGHTNESS_UP, - KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK, - KeyEvent.KEYCODE_PAIRING, - KeyEvent.KEYCODE_MEDIA_TOP_MENU, - KeyEvent.KEYCODE_11, - KeyEvent.KEYCODE_12, - KeyEvent.KEYCODE_LAST_CHANNEL, - KeyEvent.KEYCODE_TV_DATA_SERVICE, - KeyEvent.KEYCODE_VOICE_ASSIST, - KeyEvent.KEYCODE_TV_RADIO_SERVICE, - KeyEvent.KEYCODE_TV_TELETEXT, - KeyEvent.KEYCODE_TV_NUMBER_ENTRY, - KeyEvent.KEYCODE_TV_TERRESTRIAL_ANALOG, - KeyEvent.KEYCODE_TV_TERRESTRIAL_DIGITAL, - KeyEvent.KEYCODE_TV_SATELLITE, - KeyEvent.KEYCODE_TV_SATELLITE_BS, - KeyEvent.KEYCODE_TV_SATELLITE_CS, - KeyEvent.KEYCODE_TV_SATELLITE_SERVICE, - KeyEvent.KEYCODE_TV_NETWORK, - KeyEvent.KEYCODE_TV_ANTENNA_CABLE, - KeyEvent.KEYCODE_TV_INPUT_HDMI_1, - KeyEvent.KEYCODE_TV_INPUT_HDMI_2, - KeyEvent.KEYCODE_TV_INPUT_HDMI_3, - KeyEvent.KEYCODE_TV_INPUT_HDMI_4, - KeyEvent.KEYCODE_TV_INPUT_COMPOSITE_1, - KeyEvent.KEYCODE_TV_INPUT_COMPOSITE_2, - KeyEvent.KEYCODE_TV_INPUT_COMPONENT_1, - KeyEvent.KEYCODE_TV_INPUT_COMPONENT_2, - KeyEvent.KEYCODE_TV_INPUT_VGA_1, - KeyEvent.KEYCODE_TV_AUDIO_DESCRIPTION, - KeyEvent.KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP, - KeyEvent.KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN, - KeyEvent.KEYCODE_TV_ZOOM_MODE, - KeyEvent.KEYCODE_TV_CONTENTS_MENU, - KeyEvent.KEYCODE_TV_MEDIA_CONTEXT_MENU, - KeyEvent.KEYCODE_TV_TIMER_PROGRAMMING, - KeyEvent.KEYCODE_HELP, - KeyEvent.KEYCODE_NAVIGATE_PREVIOUS, - KeyEvent.KEYCODE_NAVIGATE_NEXT, - KeyEvent.KEYCODE_NAVIGATE_IN, - KeyEvent.KEYCODE_NAVIGATE_OUT, - KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, - KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, - KeyEvent.KEYCODE_MEDIA_STEP_FORWARD, - KeyEvent.KEYCODE_MEDIA_STEP_BACKWARD, - KeyEvent.KEYCODE_STEM_PRIMARY, - KeyEvent.KEYCODE_STEM_1, - KeyEvent.KEYCODE_STEM_2, - KeyEvent.KEYCODE_STEM_3, - KeyEvent.KEYCODE_DPAD_UP_LEFT, - KeyEvent.KEYCODE_DPAD_DOWN_LEFT, - KeyEvent.KEYCODE_DPAD_UP_RIGHT, - KeyEvent.KEYCODE_DPAD_DOWN_RIGHT, - KeyEvent.KEYCODE_SOFT_SLEEP, - KeyEvent.KEYCODE_CUT, - KeyEvent.KEYCODE_COPY, - KeyEvent.KEYCODE_PASTE, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT, - KeyEvent.KEYCODE_REFRESH, - KeyEvent.KEYCODE_THUMBS_UP, - KeyEvent.KEYCODE_THUMBS_DOWN, - KeyEvent.KEYCODE_PROFILE_SWITCH, - KeyEvent.KEYCODE_VIDEO_APP_1, - KeyEvent.KEYCODE_VIDEO_APP_2, - KeyEvent.KEYCODE_VIDEO_APP_3, - KeyEvent.KEYCODE_VIDEO_APP_4, - KeyEvent.KEYCODE_VIDEO_APP_5, - KeyEvent.KEYCODE_VIDEO_APP_6, - KeyEvent.KEYCODE_VIDEO_APP_7, - KeyEvent.KEYCODE_VIDEO_APP_8, - KeyEvent.KEYCODE_FEATURED_APP_1, - KeyEvent.KEYCODE_FEATURED_APP_2, - KeyEvent.KEYCODE_FEATURED_APP_3, - KeyEvent.KEYCODE_FEATURED_APP_4, - KeyEvent.KEYCODE_DEMO_APP_1, - KeyEvent.KEYCODE_DEMO_APP_2, - KeyEvent.KEYCODE_DEMO_APP_3, - KeyEvent.KEYCODE_DEMO_APP_4, - KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, - KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, - KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, - KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY, - KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY, - KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY, - KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL, - KeyEvent.KEYCODE_RECENT_APPS, - KeyEvent.KEYCODE_MACRO_1, - KeyEvent.KEYCODE_MACRO_2, - KeyEvent.KEYCODE_MACRO_3, - KeyEvent.KEYCODE_MACRO_4, - KeyEvent.KEYCODE_EMOJI_PICKER, - KeyEvent.KEYCODE_SCREENSHOT, - ).distinct().toIntArray() + private val KEYCODES: IntArray by lazy { buildKeyCodeList() } + + private fun buildKeyCodeList(): IntArray { + return IntArray(KeyEvent.getMaxKeyCode()) { it } + } val MODIFIER_KEYCODES: Set get() = setOf( @@ -387,9 +75,11 @@ object KeyEventUtils { fun modifierKeycodeToMetaState(modifier: Int) = when (modifier) { KeyEvent.KEYCODE_ALT_LEFT -> KeyEvent.META_ALT_LEFT_ON.withFlag(KeyEvent.META_ALT_ON) + KeyEvent.KEYCODE_ALT_RIGHT -> KeyEvent.META_ALT_RIGHT_ON.withFlag(KeyEvent.META_ALT_ON) KeyEvent.KEYCODE_SHIFT_LEFT -> KeyEvent.META_SHIFT_LEFT_ON.withFlag(KeyEvent.META_SHIFT_ON) + KeyEvent.KEYCODE_SHIFT_RIGHT -> KeyEvent.META_SHIFT_RIGHT_ON.withFlag( KeyEvent.META_SHIFT_ON, ) @@ -399,13 +89,17 @@ object KeyEventUtils { KeyEvent.KEYCODE_FUNCTION -> KeyEvent.META_FUNCTION_ON KeyEvent.KEYCODE_CTRL_LEFT -> KeyEvent.META_CTRL_LEFT_ON.withFlag(KeyEvent.META_CTRL_ON) + KeyEvent.KEYCODE_CTRL_RIGHT -> KeyEvent.META_CTRL_RIGHT_ON.withFlag(KeyEvent.META_CTRL_ON) KeyEvent.KEYCODE_META_LEFT -> KeyEvent.META_META_LEFT_ON.withFlag(KeyEvent.META_META_ON) + KeyEvent.KEYCODE_META_RIGHT -> KeyEvent.META_META_RIGHT_ON.withFlag(KeyEvent.META_META_ON) KeyEvent.KEYCODE_CAPS_LOCK -> KeyEvent.META_CAPS_LOCK_ON + KeyEvent.KEYCODE_NUM_LOCK -> KeyEvent.META_NUM_LOCK_ON + KeyEvent.KEYCODE_SCROLL_LOCK -> KeyEvent.META_SCROLL_LOCK_ON else -> throw Exception("can't convert modifier $modifier to meta state") From 56c8286eff99bdd8c56a7d4e57a7fb15e470179b Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 22:25:40 +0100 Subject: [PATCH 05/48] fix: do not save system bridge time relative to boot --- .../expertmode/SystemBridgeAutoStarter.kt | 11 +++++++++-- .../expertmode/SystemBridgeAutoStarterTest.kt | 19 ++++++++++++++++++- .../keymapper/base/utils/TestScopeClock.kt | 6 ++++++ .../sds100/keymapper/common/utils/Clock.kt | 6 ++++++ .../io/github/sds100/keymapper/data/Keys.kt | 4 ---- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt index cb69b61f41..5e3d150f16 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt @@ -230,7 +230,10 @@ class SystemBridgeAutoStarter @Inject constructor( return } - preferences.set(Keys.systemBridgeLastAutoStartTime, clock.elapsedRealtime()) + // This must use the unix timestamp and not a time relative to the uptime of the device. + // Otherwise, it may not autostart on reboot if it started earlier than when it last auto + // started relative to the last boot. + preferences.set(Keys.systemBridgeLastAutoStartTime, clock.unixTimestamp()) when (type) { AutoStartType.ADB -> { @@ -295,8 +298,12 @@ class SystemBridgeAutoStarter @Inject constructor( */ private suspend fun isWithinAutoStartCooldown(): Boolean { val lastAutoStartTime = preferences.get(Keys.systemBridgeLastAutoStartTime).first() + val currentTime = clock.unixTimestamp() + return lastAutoStartTime != null && - clock.elapsedRealtime() - lastAutoStartTime < (5 * 60_000) + // Check that the time is consistent. + currentTime >= lastAutoStartTime && + currentTime - lastAutoStartTime < (5 * 60) } private suspend fun isAutoStartEnabled(): Boolean { diff --git a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt index 7161d21886..b807702374 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.closeTo import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -138,6 +139,22 @@ class SystemBridgeAutoStarterTest { ) } + @Test + fun `auto start time is saved as unix timestamp`() = runTest(testDispatcher) { + fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true) + fakePreferences.set(Keys.isSystemBridgeUsed, true) + + whenever(mockSetupController.isAdbPaired()).thenReturn(true) + isWifiConnectedFlow.value = true + writeSecureSettingsGrantedFlow.value = true + + systemBridgeAutoStarter.init() + advanceUntilIdle() + + val actual = fakePreferences.get(Keys.systemBridgeLastAutoStartTime).first() + assertThat(actual!!.toDouble(), closeTo(testScopeClock.unixTimestamp().toDouble(), 5.0)) + } + @Test fun `auto start within 60 seconds of booting`() = runTest(testDispatcher) { fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true) @@ -521,7 +538,7 @@ class SystemBridgeAutoStarterTest { // It is killed unexpectedly straight after auto starting connectionStateFlow.value = SystemBridgeConnectionState.Disconnected( time = 7000, - isStoppedByUser = true, + isStoppedByUser = false, ) advanceUntilIdle() diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestScopeClock.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestScopeClock.kt index 5cb9711011..65e2f627c3 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestScopeClock.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestScopeClock.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.base.utils import io.github.sds100.keymapper.common.utils.Clock +import java.time.Instant import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.currentTime @@ -10,4 +11,9 @@ class TestScopeClock(private val testScope: TestScope) : Clock { override fun elapsedRealtime(): Long { return testScope.currentTime } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun unixTimestamp(): Long { + return Instant.now().epochSecond + } } diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/Clock.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/Clock.kt index cd98addbd5..e84b381e5b 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/Clock.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/Clock.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.common.utils import android.os.SystemClock +import java.time.Instant import javax.inject.Inject import javax.inject.Singleton @@ -9,8 +10,13 @@ class ClockImpl @Inject constructor() : Clock { override fun elapsedRealtime(): Long { return SystemClock.elapsedRealtime() } + + override fun unixTimestamp(): Long { + return Instant.now().epochSecond + } } interface Clock { fun elapsedRealtime(): Long + fun unixTimestamp(): Long } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index a51d8d1e9d..92787dc69f 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -145,10 +145,6 @@ object Keys { */ val isSystemBridgeUsed = booleanPreferencesKey("key_is_system_bridge_used") - /** - * The last time the system bridge was auto started in time since boot. - * Uses SystemClock.elapsedRealtime(). - */ val systemBridgeLastAutoStartTime = longPreferencesKey("key_system_bridge_last_auto_start_time") val keyEventActionsUseSystemBridge = From ec88303702745a49a0699db89e2ed8a36e2221ab Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 22:45:58 +0100 Subject: [PATCH 06/48] auto start expert mode if it was manually started after the last auto start --- CHANGELOG.md | 3 ++ .../expertmode/SystemBridgeAutoStarter.kt | 16 +++++++-- .../expertmode/SystemBridgeSetupUseCase.kt | 25 +++++++------ .../expertmode/SystemBridgeAutoStarterTest.kt | 35 ++++++++++++++++++- .../SystemBridgeSetupUseCaseTest.kt | 1 + .../io/github/sds100/keymapper/data/Keys.kt | 2 ++ 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a44fc81893..a3252b9084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ## Added - #1970 dynamically build the key code list so key codes in new Android releases are automatically included. +## Fixed + +- Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt index 5e3d150f16..c8c0214627 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt @@ -298,11 +298,21 @@ class SystemBridgeAutoStarter @Inject constructor( */ private suspend fun isWithinAutoStartCooldown(): Boolean { val lastAutoStartTime = preferences.get(Keys.systemBridgeLastAutoStartTime).first() + val lastManualStartTime = preferences.get(Keys.systemBridgeLastManualStartTime).first() val currentTime = clock.unixTimestamp() - return lastAutoStartTime != null && - // Check that the time is consistent. - currentTime >= lastAutoStartTime && + if (lastAutoStartTime == null) { + return false + } + + // If the user started it manually after the last auto start then ignore the cooldown. + if (lastManualStartTime != null && + lastManualStartTime >= lastAutoStartTime + ) { + return false + } + + return currentTime >= lastAutoStartTime && currentTime - lastAutoStartTime < (5 * 60) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt index 3ae7dbe92d..37fbf7319f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt @@ -4,6 +4,7 @@ import android.os.Build import android.os.Process import androidx.annotation.RequiresApi import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.common.utils.Clock import io.github.sds100.keymapper.common.utils.Constants import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.firstBlocking @@ -43,6 +44,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( private val permissionAdapter: PermissionAdapter, private val accessibilityServiceAdapter: AccessibilityServiceAdapter, private val networkAdapter: NetworkAdapter, + private val clock: Clock, ) : SystemBridgeSetupUseCase { companion object { @@ -192,27 +194,30 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( } override fun startSystemBridgeWithRoot() { - preferences.set(Keys.isSystemBridgeEmergencyKilled, false) - preferences.set(Keys.isSystemBridgeStoppedByUser, false) - systemBridgeSetupController.startWithRoot() + startSystemBridge { + systemBridgeSetupController.startWithRoot() + } } override fun startSystemBridgeWithShizuku() { - preferences.set(Keys.isSystemBridgeEmergencyKilled, false) - preferences.set(Keys.isSystemBridgeStoppedByUser, false) - systemBridgeSetupController.startWithShizuku() + startSystemBridge { systemBridgeSetupController.startWithShizuku() } } override fun startSystemBridgeWithAdb() { - preferences.set(Keys.isSystemBridgeEmergencyKilled, false) - preferences.set(Keys.isSystemBridgeStoppedByUser, false) - systemBridgeSetupController.startWithAdb() + startSystemBridge { + systemBridgeSetupController.startWithAdb() + } } override fun autoStartSystemBridgeWithAdb() { + systemBridgeSetupController.autoStartWithAdb() + } + + private fun startSystemBridge(block: () -> Unit) { + preferences.set(Keys.systemBridgeLastManualStartTime, clock.unixTimestamp()) preferences.set(Keys.isSystemBridgeEmergencyKilled, false) preferences.set(Keys.isSystemBridgeStoppedByUser, false) - systemBridgeSetupController.autoStartWithAdb() + block.invoke() } override fun isInfoDismissed(): Boolean { diff --git a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt index b807702374..1f4b61a2e0 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt @@ -522,10 +522,17 @@ class SystemBridgeAutoStarterTest { } @Test - fun `do not auto restart within 5 minutes of the last auto start`() = runTest(testDispatcher) { + fun `do not auto start within 5 minutes of the last auto start`() = runTest(testDispatcher) { fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true) fakePreferences.set(Keys.isSystemBridgeUsed, true) + fakePreferences.set(Keys.handledUpgradeToExpertMode, true) + isRootGrantedFlow.value = true + fakePreferences.set( + Keys.systemBridgeLastManualStartTime, + // 10 minutes before + testScopeClock.unixTimestamp() - 600, + ) inOrder(mockConnectionManager) { systemBridgeAutoStarter.init() @@ -547,6 +554,32 @@ class SystemBridgeAutoStarterTest { } } + @Test + fun `auto start within 5 minutes of the last auto start if the user started it manually in between`() = + runTest(testDispatcher) { + fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true) + fakePreferences.set(Keys.isSystemBridgeUsed, true) + fakePreferences.set(Keys.handledUpgradeToExpertMode, true) + + fakePreferences.set( + Keys.systemBridgeLastManualStartTime, + // 2 minutes before + testScopeClock.unixTimestamp() - 120, + ) + fakePreferences.set( + Keys.systemBridgeLastAutoStartTime, + // 3 minutes before + testScopeClock.unixTimestamp() - 180, + ) + isRootGrantedFlow.value = true + + inOrder(mockConnectionManager) { + systemBridgeAutoStarter.init() + advanceTimeBy(6000) + verify(mockConnectionManager).startWithRoot() + } + } + @Test fun `show killed and not restarting notification if want to autostart again within the cooldown`() = runTest(testDispatcher) { diff --git a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt index db43edbbc6..8c1fb04ceb 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt @@ -55,6 +55,7 @@ class SystemBridgeSetupUseCaseTest { permissionAdapter = mockPermissionAdapter, accessibilityServiceAdapter = mockAccessibilityServiceAdapter, networkAdapter = mockNetworkAdapter, + clock = mock(), ) } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 92787dc69f..65f9615f5f 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -145,6 +145,8 @@ object Keys { */ val isSystemBridgeUsed = booleanPreferencesKey("key_is_system_bridge_used") + val systemBridgeLastManualStartTime = + longPreferencesKey("key_system_bridge_last_manual_start_time") val systemBridgeLastAutoStartTime = longPreferencesKey("key_system_bridge_last_auto_start_time") val keyEventActionsUseSystemBridge = From 0f0417527f8e2b751b489e32589e807238a05c87 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 22:46:59 +0100 Subject: [PATCH 07/48] #1939 show a notification when expert mode fails to start due to being disconnected from wifi --- CHANGELOG.md | 12 +- .../expertmode/SystemBridgeAutoStarter.kt | 137 +++++++++++------- base/src/main/res/values/strings.xml | 3 + .../expertmode/SystemBridgeAutoStarterTest.kt | 32 ++++ 4 files changed, 131 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3252b9084..d116103543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ ## Added -- #1970 dynamically build the key code list so key codes in new Android releases are automatically included. +- #1970 dynamically build the key code list so key codes in new Android releases are automatically + included. +- #1939 show notification when Expert Mode fails to start due to be being disconnected from WiFi. + ## Fixed - Bugs with expert mode auto starting time. @@ -19,9 +22,11 @@ ## Bug fixes -- #1968 Device controls action no longer works on Android 16+ so it has been disabled on new Android versions. +- #1968 Device controls action no longer works on Android 16+ so it has been disabled on new Android + versions. - #1967 Still start system bridge if granting WRITE_SECURE_SETTINGS fails. -- #1965 Better system bridge support on Xiaomi devices and ask to enable "USB debugging security settings" in developer options. +- #1965 Better system bridge support on Xiaomi devices and ask to enable "USB debugging security + settings" in developer options. ## [4.0.0 Beta 5](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.05) @@ -30,6 +35,7 @@ Happy new year! ## Added + - #1947 show tip to use expert mode where the old option for screen off remapping used to be ## Bug fixes diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt index c8c0214627..411e87548c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt @@ -69,54 +69,61 @@ class SystemBridgeAutoStarter @Inject constructor( private val notificationAdapter: NotificationAdapter, private val resourceProvider: ResourceProvider, ) : ResourceProvider by resourceProvider { - enum class AutoStartType { + + private enum class AutoStartType { ADB, SHIZUKU, ROOT, } + private sealed class AutoStartEligibility { + data class Eligible(val type: AutoStartType) : AutoStartEligibility() + sealed class NotEligible : AutoStartEligibility() { + data object WiFiDisconnected : NotEligible() + data object AutoStartCooldown : NotEligible() + data object Other : NotEligible() + } + } + // Use flatMapLatest so that any calls to ADB are only done if strictly necessary. @SuppressLint("NewApi") @OptIn(ExperimentalCoroutinesApi::class) - private val autoStartTypeFlow: Flow = + private val autoStartTypeFlow: Flow = suAdapter.isRootGranted .filterNotNull() .flatMapLatest { isRooted -> if (isRooted) { - flowOf(AutoStartType.ROOT) - } else { - val useShizukuFlow = - combine( - shizukuAdapter.isStarted, - permissionAdapter.isGrantedFlow(Permission.SHIZUKU), - ) { isStarted, isGranted -> - isStarted && isGranted - } + return@flatMapLatest flowOf(AutoStartEligibility.Eligible(AutoStartType.ROOT)) + } - useShizukuFlow.flatMapLatest { useShizuku -> - if (useShizuku) { - flowOf(AutoStartType.SHIZUKU) - } else if (buildConfig.sdkInt >= Build.VERSION_CODES.R) { - val isAdbAutoStartAllowed = combine( - permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), - networkAdapter.isWifiConnected, - ) { isWriteSecureSettingsGranted, isWifiConnected -> - isWriteSecureSettingsGranted && - isWifiConnected && - setupController.isAdbPaired() - } + val useShizukuFlow = + combine( + shizukuAdapter.isStarted, + permissionAdapter.isGrantedFlow(Permission.SHIZUKU), + ) { isStarted, isGranted -> + isStarted && isGranted + } - isAdbAutoStartAllowed.distinctUntilChanged() - .map { isAdbAutoStartAllowed -> - if (isAdbAutoStartAllowed) { - AutoStartType.ADB - } else { - null - } - }.filterNotNull() - } else { - flowOf(null) + useShizukuFlow.flatMapLatest { useShizuku -> + if (useShizuku) { + flowOf(AutoStartEligibility.Eligible(AutoStartType.SHIZUKU)) + } else if (buildConfig.sdkInt >= Build.VERSION_CODES.R) { + combine( + permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), + networkAdapter.isWifiConnected, + ) { isWriteSecureSettingsGranted, isWifiConnected -> + if (!isWifiConnected) { + AutoStartEligibility.NotEligible.WiFiDisconnected + } else if (!isWriteSecureSettingsGranted) { + AutoStartEligibility.NotEligible.Other + } else if (!setupController.isAdbPaired()) { + AutoStartEligibility.NotEligible.Other + } else { + AutoStartEligibility.Eligible(AutoStartType.ADB) + } } + } else { + flowOf(AutoStartEligibility.NotEligible.Other) } } } @@ -125,7 +132,7 @@ class SystemBridgeAutoStarter @Inject constructor( * This emits values when the system bridge needs restarting after it being killed. */ @OptIn(ExperimentalCoroutinesApi::class) - private val autoStartFlow: Flow = + private val autoStartFlow: Flow = connectionManager.connectionState.flatMapLatest { connectionState -> // Do not autostart if it is connected or it was killed from the user if (connectionState !is SystemBridgeConnectionState.Disconnected || @@ -135,17 +142,9 @@ class SystemBridgeAutoStarter @Inject constructor( isSystemBridgeEmergencyKilled() || !isAutoStartEnabled() ) { - flowOf(null) + flowOf(AutoStartEligibility.NotEligible.Other) } else if (isWithinAutoStartCooldown()) { - // Do not autostart if the system bridge was killed shortly after. - // This prevents infinite loops happening. - Timber.w( - "Not auto starting the system bridge because it was last auto started less than 5 mins ago", - ) - showSystemBridgeKilledNotification( - getString(R.string.system_bridge_died_notification_not_restarting_text), - ) - flowOf(null) + flowOf(AutoStartEligibility.NotEligible.AutoStartCooldown) } else { autoStartTypeFlow } @@ -178,10 +177,28 @@ class SystemBridgeAutoStarter @Inject constructor( autoStartFlow .distinctUntilChanged() // Must come before the filterNotNull - .filterNotNull() - .collectLatest { type -> - autoStart(type) - } + .collectLatest(::processAutoStartEligibility) + } + } + + private suspend fun processAutoStartEligibility(eligibility: AutoStartEligibility) { + when (eligibility) { + is AutoStartEligibility.Eligible -> autoStart(eligibility.type) + + AutoStartEligibility.NotEligible.AutoStartCooldown -> { + // Do not autostart if the system bridge was killed shortly after. + // This prevents infinite loops happening. + Timber.w( + "Not auto starting the system bridge because it was last auto started less than 5 mins ago", + ) + showSystemBridgeKilledNotification( + getString(R.string.system_bridge_died_notification_not_restarting_text), + ) + } + + AutoStartEligibility.NotEligible.WiFiDisconnected -> showWiFiDisconnectedNotification() + + AutoStartEligibility.NotEligible.Other -> {} } } @@ -353,7 +370,27 @@ class SystemBridgeAutoStarter @Inject constructor( priority = NotificationCompat.PRIORITY_MAX, onGoing = true, showIndeterminateProgress = true, - showOnLockscreen = false, + showOnLockscreen = true, + ) + + notificationAdapter.showNotification(model) + } + + private fun showWiFiDisconnectedNotification() { + val model = NotificationModel( + id = ID_SYSTEM_BRIDGE_STATUS, + title = getString( + R.string.system_bridge_wifi_disconnected_notification_title, + ), + text = getString(R.string.system_bridge_wifi_disconnected_notification_text), + onClickAction = KMNotificationAction.Activity.MainActivity( + BaseMainActivity.ACTION_START_SYSTEM_BRIDGE, + ), + channel = CHANNEL_SETUP_ASSISTANT, + icon = R.drawable.offline_bolt_24px, + priority = NotificationCompat.PRIORITY_MAX, + onGoing = false, + showOnLockscreen = true, ) notificationAdapter.showNotification(model) @@ -371,7 +408,7 @@ class SystemBridgeAutoStarter @Inject constructor( channel = CHANNEL_SETUP_ASSISTANT, icon = R.drawable.offline_bolt_24px, onGoing = false, - showOnLockscreen = false, + showOnLockscreen = true, autoCancel = true, priority = NotificationCompat.PRIORITY_MAX, onClickAction = KMNotificationAction.Activity.MainActivity( diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 72a2f782f4..1e5a335be7 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1776,6 +1776,9 @@ Automatically restarting… Not auto restarting because last auto started less than 5 minutes ago. If you\'re not killing the service report the issue to the developer. + Starting Expert mode failed + Your phone must be connected to a WiFi network + Discover What do you want to remap? diff --git a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt index 1f4b61a2e0..39f34d3b4a 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt @@ -1,10 +1,14 @@ package io.github.sds100.keymapper.base.expertmode +import androidx.core.app.NotificationCompat +import io.github.sds100.keymapper.base.BaseMainActivity import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.repositories.FakePreferenceRepository +import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.TestBuildConfigProvider import io.github.sds100.keymapper.base.utils.TestScopeClock import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState @@ -464,6 +468,34 @@ class SystemBridgeAutoStarterTest { } } + @Test + fun `show wifi disconnected notification when auto starting`() = runTest(testDispatcher) { + fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true) + fakePreferences.set(Keys.isSystemBridgeUsed, true) + isWifiConnectedFlow.value = false + writeSecureSettingsGrantedFlow.value = true + + inOrder(mockNotificationAdapter) { + systemBridgeAutoStarter.init() + advanceTimeBy(10000) + + val expectedModel = NotificationModel( + id = NotificationController.ID_SYSTEM_BRIDGE_STATUS, + channel = NotificationController.CHANNEL_SETUP_ASSISTANT, + title = "test_string", + text = "test_string", + icon = R.drawable.offline_bolt_24px, + onClickAction = KMNotificationAction.Activity.MainActivity( + action = BaseMainActivity.ACTION_START_SYSTEM_BRIDGE, + ), + priority = NotificationCompat.PRIORITY_MAX, + showOnLockscreen = true, + onGoing = false, + ) + verify(mockNotificationAdapter).showNotification(expectedModel) + } + } + @Test fun `show failed notification when connection times out`() = runTest(testDispatcher) { isRootGrantedFlow.value = true From 4e63c1c716fd154a9ba039b0a034f5abc371dde1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 22:52:24 +0100 Subject: [PATCH 08/48] #1986 fix: trigger screen is usable on slightly rectangular screens with a low DPI --- CHANGELOG.md | 1 + .../base/trigger/BaseTriggerScreen.kt | 28 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d116103543..5b7c55f5e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ## Fixed +- #1986 fix: trigger screen is usable on slightly rectangular screens with a low DPI - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) 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 6be1f4e4a4..01b19e820a 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 @@ -21,6 +21,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -183,7 +184,8 @@ fun BaseTriggerScreen( private fun isHorizontalLayout(): Boolean { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - return windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT + return windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT && + windowSizeClass.isWidthAtLeastBreakpoint(600) } @Composable @@ -657,6 +659,30 @@ private fun VerticalPreviewTiny() { } } +// This preview is slightly rectangular +@Preview(heightDp = 500, widthDp = 530) +@Composable +private fun PreviewSquareRectangle() { + KeyMapperTheme { + if (isHorizontalLayout()) { + Text("MUST BE VERTICAL LAYOUT!") + } else { + TriggerScreenVertical( + configState = previewState, + recordTriggerState = RecordTriggerState.Idle, + expertModeSwitchState = ExpertModeRecordSwitchState( + isVisible = true, + isChecked = true, + isEnabled = true, + ), + discoverScreenContent = { + TriggerDiscoverScreen() + }, + ) + } + } +} + @Preview(device = Devices.PIXEL) @Composable private fun VerticalEmptyPreview() { From ca68a6813702e0d8a025d07bf8a425f185f0e387 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 23:13:26 +0100 Subject: [PATCH 09/48] fix: refresh the system bridge starter script when the app launches in case the paths changed --- .../manager/SystemBridgeConnectionManager.kt | 14 +++++++++++++- .../sysbridge/starter/SystemBridgeStarter.kt | 10 +++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 66274d0e96..cdca17caf7 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -67,7 +67,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( time = SystemClock.elapsedRealtime(), // Get whether the user previously stopped the system bridge. isStoppedByUser = - preferences.get(Keys.isSystemBridgeStoppedByUser).firstBlocking() ?: false, + preferences.get(Keys.isSystemBridgeStoppedByUser).firstBlocking() ?: false, ), ) private var isExpectedDeath: Boolean = false @@ -93,6 +93,18 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( } } + init { + // Refresh the starter script because the paths to the apk and libs may + // have changed. + coroutineScope.launch { + try { + starter.refreshStarterScript() + } catch (e: Exception) { + Timber.e("Failed to refresh system bridge starter script", e) + } + } + } + fun pingBinder(): Boolean { synchronized(systemBridgeLock) { return systemBridgeFlow.value?.asBinder()?.pingBinder() == true diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index d95b6623e6..0d5429a851 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -170,11 +170,19 @@ class SystemBridgeStarter @Inject constructor( } } + suspend fun refreshStarterScript() { + writeStarterScript() + } + /** * Get the shell command that can be used to start the system bridge manually. * This command should be executed with 'adb shell'. */ suspend fun getStartCommand(): KMResult { + return writeStarterScript().then { starterPath -> Success("sh $starterPath") } + } + + private suspend fun writeStarterScript(): KMResult { val directory = if (buildConfigProvider.sdkInt > Build.VERSION_CODES.R) { try { ctx.getExternalFilesDir(null)?.parentFile @@ -196,7 +204,7 @@ class SystemBridgeStarter @Inject constructor( protectedStorageDir } - return copyStarterFiles(directory!!).then { starterPath -> Success("sh $starterPath") } + return copyStarterFiles(directory!!) } /** From 2eab0fc90ac902b0a2827928d4b140240ad38a78 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 23:23:23 +0100 Subject: [PATCH 10/48] #1972 fix: expert mode works on Android 10 --- CHANGELOG.md | 3 +- .../sysbridge/service/SystemBridge.kt | 209 +++++++++--------- 2 files changed, 111 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7c55f5e1..bd65d62eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ ## Fixed -- #1986 fix: trigger screen is usable on slightly rectangular screens with a low DPI +- #1986 trigger screen is usable on slightly rectangular screens with a low DPI +- #1972 Expert Mode works on Android 10 - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt index edfbbe4ab6..2aa3886f91 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -271,115 +271,113 @@ class SystemBridge : ISystemBridge.Stub() { } } - private val inputManager: IInputManager - private val wifiManager: IWifiManager? - private val permissionManager: IPermissionManager - private val telephonyManager: ITelephony? - private val packageManager: IPackageManager - private val bluetoothManager: IBluetoothManager? - private val nfcAdapter: INfcAdapter? - private val connectivityManager: IConnectivityManager? - private val tetheringConnector: ITetheringConnector? - private val activityManager: IActivityManager - private val activityTaskManager: IActivityTaskManager - private val audioService: IAudioService? - private val usbManager: IUsbManager? - - private val processPackageName: String = when (Process.myUid()) { - Process.ROOT_UID -> "root" - Process.SHELL_UID -> "com.android.shell" - else -> throw IllegalStateException("SystemBridge must run as root or shell user") - } - - init { - if (versionCode == -1) { - Log.e(TAG, "SystemBridge version code not set") - throw IllegalStateException("SystemBridge version code not set") - } - - val libraryPath = System.getProperty("keymapper_sysbridge.library.path") - @SuppressLint("UnsafeDynamicallyLoadedCode") - System.load("$libraryPath/libevdev_manager.so") - - Log.i(TAG, "SystemBridge starting... Version code $versionCode") - - waitSystemService(Context.ACTIVITY_SERVICE) - activityManager = IActivityManager.Stub.asInterface( - ServiceManager.getService(Context.ACTIVITY_SERVICE), - ) - - waitSystemService("activity_task") - activityTaskManager = IActivityTaskManager.Stub.asInterface( - ServiceManager.getService("activity_task"), - ) - - waitSystemService(Context.USER_SERVICE) - waitSystemService(Context.APP_OPS_SERVICE) - - waitSystemService("package") - packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) - - waitSystemService("permissionmgr") - permissionManager = - IPermissionManager.Stub.asInterface(ServiceManager.getService("permissionmgr")) - + private val inputManager: IInputManager by lazy { waitSystemService(Context.INPUT_SERVICE) - inputManager = - IInputManager.Stub.asInterface(ServiceManager.getService(Context.INPUT_SERVICE)) + IInputManager.Stub.asInterface(ServiceManager.getService(Context.INPUT_SERVICE)) + } + private val wifiManager: IWifiManager? by lazy { if (hasSystemFeature(PackageManager.FEATURE_WIFI)) { waitSystemService(Context.WIFI_SERVICE) - wifiManager = - IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) + IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) } else { - wifiManager = null + null } + } + + private val permissionManager: IPermissionManager? by lazy { + waitSystemService("permissionmgr") + IPermissionManager.Stub.asInterface(ServiceManager.getService("permissionmgr")) + } + private val telephonyManager: ITelephony? by lazy { if (hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { waitSystemService(Context.TELEPHONY_SERVICE) - telephonyManager = - ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)) + ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)) } else { - telephonyManager = null + null } + } + + private val packageManager: IPackageManager by lazy { + waitSystemService("package") + IPackageManager.Stub.asInterface(ServiceManager.getService("package")) + } + private val bluetoothManager: IBluetoothManager? by lazy { if (hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) { waitSystemService("bluetooth_manager") - bluetoothManager = - IBluetoothManager.Stub.asInterface(ServiceManager.getService("bluetooth_manager")) + IBluetoothManager.Stub.asInterface(ServiceManager.getService("bluetooth_manager")) } else { - bluetoothManager = null + null } + } + private val nfcAdapter: INfcAdapter? by lazy { if (hasSystemFeature(PackageManager.FEATURE_NFC)) { waitSystemService(Context.NFC_SERVICE) - nfcAdapter = - INfcAdapter.Stub.asInterface(ServiceManager.getService(Context.NFC_SERVICE)) + INfcAdapter.Stub.asInterface(ServiceManager.getService(Context.NFC_SERVICE)) } else { - nfcAdapter = null + null } + } - waitSystemService(Context.CONNECTIVITY_SERVICE) - connectivityManager = - IConnectivityManager.Stub.asInterface( - ServiceManager.getService(Context.CONNECTIVITY_SERVICE), - ) + private val connectivityManager: IConnectivityManager? by lazy { - waitSystemService(Context.AUDIO_SERVICE) - audioService = - IAudioService.Stub.asInterface(ServiceManager.getService(Context.AUDIO_SERVICE)) + waitSystemService(Context.CONNECTIVITY_SERVICE) + IConnectivityManager.Stub.asInterface( + ServiceManager.getService(Context.CONNECTIVITY_SERVICE), + ) + } + private val tetheringConnector: ITetheringConnector? by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { waitSystemService("tethering") - tetheringConnector = - ITetheringConnector.Stub.asInterface(ServiceManager.getService("tethering")) + ITetheringConnector.Stub.asInterface(ServiceManager.getService("tethering")) } else { - tetheringConnector = null + null } + } + private val activityManager: IActivityManager + private val activityTaskManager: IActivityTaskManager by lazy { + waitSystemService("activity_task") + IActivityTaskManager.Stub.asInterface( + ServiceManager.getService("activity_task"), + ) + } + + private val audioService: IAudioService? by lazy { + waitSystemService(Context.AUDIO_SERVICE) + IAudioService.Stub.asInterface(ServiceManager.getService(Context.AUDIO_SERVICE)) + } + private val usbManager: IUsbManager? by lazy { waitSystemService(Context.USB_SERVICE) - usbManager = - IUsbManager.Stub.asInterface(ServiceManager.getService(Context.USB_SERVICE)) + IUsbManager.Stub.asInterface(ServiceManager.getService(Context.USB_SERVICE)) + } + + private val processPackageName: String = when (Process.myUid()) { + Process.ROOT_UID -> "root" + Process.SHELL_UID -> "com.android.shell" + else -> throw IllegalStateException("SystemBridge must run as root or shell user") + } + + init { + if (versionCode == -1) { + Log.e(TAG, "SystemBridge version code not set") + throw IllegalStateException("SystemBridge version code not set") + } + + val libraryPath = System.getProperty("keymapper_sysbridge.library.path") + @SuppressLint("UnsafeDynamicallyLoadedCode") + System.load("$libraryPath/libevdev_manager.so") + + Log.i(TAG, "SystemBridge starting... Version code $versionCode") + + waitSystemService(Context.ACTIVITY_SERVICE) + activityManager = IActivityManager.Stub.asInterface( + ServiceManager.getService(Context.ACTIVITY_SERVICE), + ) val applicationInfo = getKeyMapperPackageInfo() @@ -396,6 +394,9 @@ class SystemBridge : ISystemBridge.Stub() { initEvdevManager() + waitSystemService(Context.USER_SERVICE) + waitSystemService(Context.APP_OPS_SERVICE) + Log.i(TAG, "SystemBridge started complete. Version code $versionCode") } @@ -497,7 +498,7 @@ class SystemBridge : ISystemBridge.Stub() { throw UnsupportedOperationException("WiFi not supported") } - return wifiManager.setWifiEnabled(processPackageName, enable) + return wifiManager!!.setWifiEnabled(processPackageName, enable) } override fun writeEvdevEvent(deviceId: Int, type: Int, code: Int, value: Int): Boolean { @@ -512,17 +513,25 @@ class SystemBridge : ISystemBridge.Stub() { return Process.myUid() } - @RequiresApi(Build.VERSION_CODES.R) override fun grantPermission(permission: String?, deviceId: Int) { val userId = UserHandleUtils.getCallingUserId() - PermissionManagerApis.grantPermission( - permissionManager, - systemBridgePackageName ?: return, - permission ?: return, - deviceId, - userId, - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + PermissionManagerApis.grantPermission( + permissionManager!!, + systemBridgePackageName ?: return, + permission ?: return, + deviceId, + userId, + ) + } else { + PermissionManagerApis.grantPermission( + packageManager, + systemBridgePackageName ?: return, + permission ?: return, + userId, + ) + } } private fun sendBinderToApp(): Boolean { @@ -676,14 +685,14 @@ class SystemBridge : ISystemBridge.Stub() { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - telephonyManager.setDataEnabledForReason( + telephonyManager!!.setDataEnabledForReason( subId, DATA_ENABLED_REASON_USER, enable, processPackageName, ) } else { - telephonyManager.setUserDataEnabled(subId, enable) + telephonyManager!!.setUserDataEnabled(subId, enable) } } @@ -709,9 +718,9 @@ class SystemBridge : ISystemBridge.Stub() { val attributionSource = attributionSourceBuilder.build() if (enable) { - bluetoothManager.enable(attributionSource) + bluetoothManager!!.enable(attributionSource) } else { - bluetoothManager.disable(attributionSource, true) + bluetoothManager!!.disable(attributionSource, true) } } @@ -721,10 +730,10 @@ class SystemBridge : ISystemBridge.Stub() { } if (enable) { - NfcAdapterApis.enable(nfcAdapter, processPackageName) + NfcAdapterApis.enable(nfcAdapter!!, processPackageName) } else { NfcAdapterApis.disable( - adapter = nfcAdapter, + adapter = nfcAdapter!!, saveState = true, packageName = processPackageName, ) @@ -736,7 +745,7 @@ class SystemBridge : ISystemBridge.Stub() { throw UnsupportedOperationException("ConnectivityManager not supported") } - connectivityManager.setAirplaneMode(enable) + connectivityManager!!.setAirplaneMode(enable) } override fun forceStopPackage(packageName: String?) { @@ -767,7 +776,7 @@ class SystemBridge : ISystemBridge.Stub() { throw UnsupportedOperationException("AudioService not supported") } - audioService.setRingerModeInternal(ringerMode, processPackageName) + audioService!!.setRingerModeInternal(ringerMode, processPackageName) } @RequiresApi(Build.VERSION_CODES.R) @@ -806,7 +815,7 @@ class SystemBridge : ISystemBridge.Stub() { // instead of keeping it registered for the lifetime of SystemBridge. This is // a safety measure in case there's a bug in the callback that could crash // the entire SystemBridge process. - tetheringConnector.registerTetheringEventCallback(callback, processPackageName) + tetheringConnector!!.registerTetheringEventCallback(callback, processPackageName) // Wait for callback with timeout using Handler mainHandler.postDelayed( @@ -824,7 +833,7 @@ class SystemBridge : ISystemBridge.Stub() { } catch (e: InterruptedException) { Thread.currentThread().interrupt() } finally { - tetheringConnector.unregisterTetheringEventCallback(callback, processPackageName) + tetheringConnector!!.unregisterTetheringEventCallback(callback, processPackageName) } return result @@ -848,9 +857,9 @@ class SystemBridge : ISystemBridge.Stub() { connectivityScope = 1 } - tetheringConnector.startTethering(request, processPackageName, null, null) + tetheringConnector!!.startTethering(request, processPackageName, null, null) } else { - tetheringConnector.stopTethering(TETHERING_WIFI, processPackageName, null, null) + tetheringConnector!!.stopTethering(TETHERING_WIFI, processPackageName, null, null) } } From 03cb80cba7429c1b7c12f4f44f09b86525d4be07 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 23:32:17 +0100 Subject: [PATCH 11/48] #1976 fix: panic in Rust system bridge code when unwrapping /dev/input path for uinput device --- CHANGELOG.md | 5 +++-- .../evdev_manager/core/src/evdev_grab_controller.rs | 12 ++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd65d62eed..1981720664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,9 @@ ## Fixed -- #1986 trigger screen is usable on slightly rectangular screens with a low DPI -- #1972 Expert Mode works on Android 10 +- #1986 trigger screen is usable on slightly rectangular screens with a low DPI. +- #1972 Expert Mode works on Android 10. +- #1976 Panic in Rust system bridge code on some devices. - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs b/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs index 848b0d0f9c..78c4d4a8cb 100644 --- a/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs +++ b/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs @@ -274,7 +274,7 @@ impl EvdevGrabController { ) -> Result, EvdevError> { let uinput_paths: Vec = grabbed_devices .iter() - .map(|(_, device)| device.uinput.devnode().unwrap().into()) + .filter_map(|(_, device)| device.uinput.devnode().map(|node| PathBuf::from(node))) .collect(); let mut paths: Vec = Vec::new(); @@ -340,9 +340,13 @@ impl EvdevGrabController { impl InotifyCallback for EvdevGrabController { fn on_inotify_dev_input(&self, paths: &[PathBuf]) { let mut grabbed_devices = self.grabbed_devices.write().unwrap(); - let is_uinput_device = grabbed_devices - .iter() - .any(|(_, device)| paths.contains(&device.uinput.devnode().unwrap().into())); + let is_uinput_device = + grabbed_devices + .iter() + .any(|(_, device)| match &device.uinput.devnode() { + None => false, + Some(dev_node) => paths.contains(&dev_node.into()), + }); if is_uinput_device { return; From e85fa179debca75fcce16479eb834422e84defae Mon Sep 17 00:00:00 2001 From: Jack Ambler <54366245+jambl3r@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:46:16 +0000 Subject: [PATCH 12/48] Update README.md to not exclude mouse buttons --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 473668047b..39e3af76e0 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ Key Mapper supports a huge variety of buttons and keys: - ALL your phone buttons (volume AND side key) - Game controllers (D-pad, ABXY, and most others) - Keyboards +- Mouse buttons - Headsets and headphones - Fingerprint sensor Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. Not currently supported: - - Mouse buttons - Joysticks and triggers (LT,RT) on gamepads Not enough keys? Design your own on-screen button layouts and remap those just like real keys! From 79f4fea93176fd8b3a7e9decd74d1794242289b0 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 23:46:44 +0100 Subject: [PATCH 13/48] #1971 fix: media actions work again in some apps, like YouTube --- CHANGELOG.md | 1 + .../system/media/AndroidMediaAdapter.kt | 36 +++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1981720664..c65985dcdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - #1986 trigger screen is usable on slightly rectangular screens with a low DPI. - #1972 Expert Mode works on Android 10. - #1976 Panic in Rust system bridge code on some devices. +- #1971 Media actions work again in some apps, like YouTube. - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt index 0c8e6458e8..902f85ed7d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt @@ -9,6 +9,7 @@ import android.media.MediaPlayer import android.media.session.MediaController import android.media.session.MediaSessionManager import android.media.session.PlaybackState +import android.view.KeyEvent import androidx.core.content.getSystemService import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext @@ -87,7 +88,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.fastForward() return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, packageName) } } @@ -98,7 +99,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.rewind() return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND, packageName) } } @@ -109,7 +110,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.play() return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY, packageName) } } @@ -120,7 +121,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.pause() return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE, packageName) } } @@ -135,7 +136,7 @@ class AndroidMediaAdapter @Inject constructor( } return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, packageName) } } @@ -146,7 +147,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.skipToPrevious() return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS, packageName) } } @@ -157,7 +158,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.skipToNext() return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT, packageName) } } @@ -168,7 +169,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.stop() return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP, packageName) } } @@ -180,7 +181,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.seekTo(position + SEEK_AMOUNT) return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STEP_FORWARD, packageName) } } @@ -192,7 +193,7 @@ class AndroidMediaAdapter @Inject constructor( session.transportControls.seekTo(max(0, position - SEEK_AMOUNT)) return Success(Unit) } else { - return KMError.MediaActionUnsupported + return sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STEP_BACKWARD, packageName) } } @@ -277,6 +278,21 @@ class AndroidMediaAdapter @Inject constructor( activeMediaSessions.update { mediaSessions } } + // Important! See issue #1971. Some apps do not expose media controls or say they support + // media control actions but they still respond to key events. + private fun sendMediaKeyEvent(keyCode: Int, packageName: String?): KMResult<*> { + if (packageName == null) { + audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) + audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode)) + } else { + val session = getPackageMediaSession(packageName) ?: return KMError.NoMediaSessions + session.dispatchMediaButtonEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) + session.dispatchMediaButtonEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode)) + } + + return Success(Unit) + } + private fun MediaController.isPlaybackActionSupported(action: Long): Boolean = (playbackState?.actions ?: 0) and action != 0L From 0213f49c998bd266be4b7ae79a9afeca61468732 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 23:47:46 +0100 Subject: [PATCH 14/48] style: reformat --- .../sysbridge/manager/SystemBridgeConnectionManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index cdca17caf7..12453dca8d 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -67,7 +67,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( time = SystemClock.elapsedRealtime(), // Get whether the user previously stopped the system bridge. isStoppedByUser = - preferences.get(Keys.isSystemBridgeStoppedByUser).firstBlocking() ?: false, + preferences.get(Keys.isSystemBridgeStoppedByUser).firstBlocking() ?: false, ), ) private var isExpectedDeath: Boolean = false From b0f86bfdaad8cd27d4cf977e4ffb97a6f5037ba1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 23:49:14 +0100 Subject: [PATCH 15/48] cargo reformat --- .../core/src/android/keylayout/generic_key_layout.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs index b454076831..ac7f35eb89 100644 --- a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs +++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs @@ -286,4 +286,3 @@ axis 0x0a LTRIGGER axis 0x10 HAT_X axis 0x11 HAT_Y "#; - From 59f33693a5a2fd7f9e7eed09cdfb6bec9878cf23 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 23:54:30 +0100 Subject: [PATCH 16/48] #1984 do not show "use expert mode" button for tips if expert mode already enabled --- .../base/onboarding/OnboardingTipDelegate.kt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt index da7f2078fd..9b3f8bd802 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingTipDelegate.kt @@ -21,6 +21,8 @@ import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.data.utils.PrefDelegate +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.isConnected import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import javax.inject.Inject import javax.inject.Named @@ -37,6 +39,7 @@ class OnboardingTipDelegateImpl @Inject constructor( private val preferenceRepository: PreferenceRepository, private val configTriggerUseCase: ConfigTriggerUseCase, private val configActionsUseCase: ConfigActionsUseCase, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, ) : OnboardingTipDelegate, @@ -225,7 +228,13 @@ class OnboardingTipDelegateImpl @Inject constructor( title = getString(R.string.tip_volume_buttons_expert_mode_title), message = getString(R.string.tip_volume_buttons_expert_mode_text), isDismissable = true, - buttonText = getString(R.string.tip_volume_buttons_expert_mode_button), + buttonText = if (systemBridgeConnectionManager.isConnected()) { + null + } else { + getString( + R.string.tip_volume_buttons_expert_mode_button, + ) + }, ) triggerTip.value = tip } @@ -236,7 +245,13 @@ class OnboardingTipDelegateImpl @Inject constructor( title = getString(R.string.tip_caps_lock_expert_mode_title), message = getString(R.string.tip_caps_lock_expert_mode_text), isDismissable = true, - buttonText = getString(R.string.tip_caps_lock_expert_mode_button), + buttonText = if (systemBridgeConnectionManager.isConnected()) { + null + } else { + getString( + R.string.tip_caps_lock_expert_mode_button, + ) + }, ) triggerTip.value = tip } @@ -334,7 +349,13 @@ class OnboardingTipDelegateImpl @Inject constructor( title = getString(R.string.tip_ringer_mode_title), message = getString(R.string.tip_ringer_mode_text), isDismissable = true, - buttonText = getString(R.string.tip_ringer_mode_button), + buttonText = if (systemBridgeConnectionManager.isConnected()) { + null + } else { + getString( + R.string.tip_ringer_mode_button, + ) + }, ) actionsTip.value = tip } From 09738d6556adb02bd269e9ed57ceb004b42d2501 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 16 Jan 2026 23:59:45 +0100 Subject: [PATCH 17/48] #1977 potentially reduce the amount of "Failed to discover ADB port" errors --- .../sds100/keymapper/sysbridge/adb/AdbMdns.kt | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt index e63621a0bd..ff16132065 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt @@ -17,8 +17,6 @@ import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import timber.log.Timber @@ -40,8 +38,6 @@ internal class AdbMdns(ctx: Context, private val serviceType: AdbServiceType) { private var serviceResolvedChannel: Channel? = null private val isDiscovering: MutableStateFlow = MutableStateFlow(false) - private val discoveredPort: MutableStateFlow = MutableStateFlow(null) - private val discoverMutex: Mutex = Mutex() private val resolveListener: NsdManager.ResolveListener = object : NsdManager.ResolveListener { override fun onResolveFailed(nsdServiceInfo: NsdServiceInfo, i: Int) { @@ -106,19 +102,10 @@ internal class AdbMdns(ctx: Context, private val serviceType: AdbServiceType) { @OptIn(ExperimentalCoroutinesApi::class) suspend fun discoverPort(): Int? { - discoverMutex.withLock { - val currentPort = discoveredPort.value - - if (currentPort == null || !isPortAvailable(currentPort)) { - val port = withContext(Dispatchers.IO) { - discoverPortInternal() - } - discoveredPort.value = port - return port - } else { - return currentPort - } + val port = withContext(Dispatchers.IO) { + discoverPortInternal() } + return port } @OptIn(ExperimentalCoroutinesApi::class) From c8e85dce2c3e329b50b0f9b0fbe5608a9c249c71 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 00:18:55 +0100 Subject: [PATCH 18/48] style: reformat --- evdev/src/main/rust/evdev/.cargo/config.toml | 1 + evdev/src/main/rust/evdev_manager/.cargo/config.toml | 1 + .../core/src/android/keylayout/generic_key_layout.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/evdev/src/main/rust/evdev/.cargo/config.toml b/evdev/src/main/rust/evdev/.cargo/config.toml index 7c6f86610a..48e2761303 100644 --- a/evdev/src/main/rust/evdev/.cargo/config.toml +++ b/evdev/src/main/rust/evdev/.cargo/config.toml @@ -14,3 +14,4 @@ rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] + diff --git a/evdev/src/main/rust/evdev_manager/.cargo/config.toml b/evdev/src/main/rust/evdev_manager/.cargo/config.toml index 7c6f86610a..48e2761303 100644 --- a/evdev/src/main/rust/evdev_manager/.cargo/config.toml +++ b/evdev/src/main/rust/evdev_manager/.cargo/config.toml @@ -14,3 +14,4 @@ rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] + diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs index ac7f35eb89..b454076831 100644 --- a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs +++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs @@ -286,3 +286,4 @@ axis 0x0a LTRIGGER axis 0x10 HAT_X axis 0x11 HAT_Y "#; + From b50092d5e86efa9385cb608ad7100f40b779079a Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 00:25:27 +0100 Subject: [PATCH 19/48] chore: upgrade android gradle plugin --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81be2676ca..8caa284e77 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ min-sdk = "26" build-tools = "36.1.0" target-sdk = "36" -android-gradle-plugin = "8.9.1" +android-gradle-plugin = "8.13.2" androidx-activity = "1.10.1" androidx-annotation = "1.9.1" androidx-appcompat = "1.7.0" From 8e1ee50957a0b49a0cdf3367d0a5d39d1f7d8da6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 13:15:57 +0100 Subject: [PATCH 20/48] #1973 add option to change app language on Android 13+ --- CHANGELOG.md | 1 + app/build.gradle.kts | 4 + .../github/sds100/keymapper/MainActivity.kt | 1 + app/src/main/res/resources.properties | 1 + base/src/main/AndroidManifest.xml | 11 +- .../sds100/keymapper/base/BaseMainActivity.kt | 5 + .../keymapper/base/BaseSingletonHiltModule.kt | 7 + .../base/expertmode/ExpertModeScreen.kt | 2 +- .../base/settings/AppLocaleAdapter.kt | 121 ++++++++++++++++++ .../base/settings/ConfigSettingsUseCase.kt | 31 +++++ .../keymapper/base/settings/SettingsScreen.kt | 23 ++++ .../base/settings/SettingsViewModel.kt | 17 ++- base/src/main/res/values/strings.xml | 5 + .../keymapper/common/utils/SettingsUtils.kt | 29 ++++- .../service/SystemBridgeSetupController.kt | 4 +- 15 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 app/src/main/res/resources.properties create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/settings/AppLocaleAdapter.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index c65985dcdd..b7cffe8079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - #1970 dynamically build the key code list so key codes in new Android releases are automatically included. - #1939 show notification when Expert Mode fails to start due to be being disconnected from WiFi. +- #1973 add setting to change app language on Android 13+. ## Fixed diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d234f10e3..a0083b73db 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,6 +91,10 @@ android { } } + androidResources { + generateLocaleConfig = true + } + buildFeatures { dataBinding = true aidl = true diff --git a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt b/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt index d48551a3dc..278186a9e1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.BaseMainActivity import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.databinding.ActivityMainBinding +import io.github.sds100.keymapper.base.settings.AppLocaleAdapter import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.showDialogs import javax.inject.Inject diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000000..d5a3ddc92a --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US \ No newline at end of file diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index faddfc687d..7b945089d6 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ - + + + + + \ No newline at end of file diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index b726317db1..b8b86e3a43 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -29,6 +29,7 @@ import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.input.InputEventHubImpl import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase +import io.github.sds100.keymapper.base.settings.AppLocaleAdapterImpl import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider @@ -104,6 +105,9 @@ abstract class BaseMainActivity : AppCompatActivity() { @Inject lateinit var configKeyMapState: ConfigKeyMapStateImpl + @Inject + lateinit var appLocaleAdapter: AppLocaleAdapterImpl + private lateinit var requestPermissionDelegate: RequestPermissionDelegate private val currentNightMode: Int @@ -201,6 +205,7 @@ abstract class BaseMainActivity : AppCompatActivity() { systemBridgeSetupController.invalidateSettings() networkAdapter.invalidateState() onboardingUseCase.handledMigrateScreenOffKeyMapsNotification() + appLocaleAdapter.invalidate() } override fun onSaveInstanceState(outState: Bundle) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt index 435a7e3265..66d2207e9b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.base import dagger.Binds import dagger.Module import dagger.hilt.InstallIn +import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.components.SingletonComponent import io.github.sds100.keymapper.base.actions.GetActionErrorUseCase import io.github.sds100.keymapper.base.actions.GetActionErrorUseCaseImpl @@ -30,6 +31,8 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCaseImpl import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegate import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegateImpl +import io.github.sds100.keymapper.base.settings.AppLocaleAdapter +import io.github.sds100.keymapper.base.settings.AppLocaleAdapterImpl import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCaseImpl @@ -208,4 +211,8 @@ abstract class BaseSingletonHiltModule { @Binds @Singleton abstract fun bindClock(impl: ClockImpl): Clock + + @Binds + @Singleton + abstract fun bindAppLocaleAdapter(impl: AppLocaleAdapterImpl): AppLocaleAdapter } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt index 5275f2fbc9..8d75d6dbc0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt @@ -516,7 +516,7 @@ private fun IncompatibleUsbModeCard(modifier: Modifier = Modifier) { SettingsUtils.launchSettingsScreen( ctx, Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS, - "default_usb_configuration", + fragmentArg = "default_usb_configuration", ) }, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/AppLocaleAdapter.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/AppLocaleAdapter.kt new file mode 100644 index 0000000000..ccb95a58e9 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/AppLocaleAdapter.kt @@ -0,0 +1,121 @@ +package io.github.sds100.keymapper.base.settings + +import android.content.Context +import android.os.Build +import android.provider.Settings +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.net.toUri +import androidx.core.os.LocaleListCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.common.utils.SettingsUtils +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@Singleton +class AppLocaleAdapterImpl @Inject constructor( + @param:ApplicationContext private val ctx: Context, +) : AppLocaleAdapter { + + private val _currentLocaleDisplayName = MutableStateFlow(getLocaleDisplayName()) + override val currentLocaleDisplayName: StateFlow = + _currentLocaleDisplayName.asStateFlow() + + fun invalidate() { + _currentLocaleDisplayName.value = getLocaleDisplayName() + } + + private fun getLocaleDisplayName(): String? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return null + } + + val locales = AppCompatDelegate.getApplicationLocales() + return if (locales.isEmpty) { + ctx.getString(R.string.language_system_default) + } else { + locales.get(0)?.displayName + } + } + + override fun launchAppLocaleSettingsScreen(): Boolean { + return SettingsUtils.launchSettingsScreen( + ctx, + Settings.ACTION_APP_LOCALE_SETTINGS, + uri = "package:${ctx.packageName}".toUri(), + ) + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun getSupportedLocales(): List { + return buildList { + val localeList = AppCompatDelegate.getApplicationLocales() + for (i in 0 until localeList.size()) { + val locale = localeList.get(i) ?: continue + + add(AppLocaleOption(locale.toLanguageTag(), locale.displayName)) + } + } + } + + override fun getCurrentLocale(): String? { + val locales = AppCompatDelegate.getApplicationLocales() + return if (locales.isEmpty) { + null + } else { + locales.toLanguageTags() + } + } + + override fun setLocale(localeTag: String?) { + val localeList = if (localeTag.isNullOrEmpty()) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(localeTag) + } + AppCompatDelegate.setApplicationLocales(localeList) + } +} + +/** + * Represents a locale option for the language picker. + * @param tag The locale tag (e.g., "en", "pt") or null for system default. + * @param displayName The display name shown to users. + */ +data class AppLocaleOption(val tag: String?, val displayName: String) + +/** + * Adapter to manage app locale settings using AndroidX AppCompatDelegate. + * This integrates with Android 13+ per-app language preferences and provides + * backward compatibility for older Android versions. + */ +interface AppLocaleAdapter { + /** + * The display name of the current locale, or null if not supported (Android < 13). + */ + val currentLocaleDisplayName: StateFlow + + fun launchAppLocaleSettingsScreen(): Boolean + + /** + * Get list of supported locales by parsing the locales_config.xml file. + * Returns a list of AppLocaleOption with locale tag and display name. + */ + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + fun getSupportedLocales(): List + + /** + * Get the currently selected locale tag, or null if using system default. + */ + fun getCurrentLocale(): String? + + /** + * Set the app locale. + * @param localeTag The locale tag (e.g., "en", "pt", "tr") or null for system default. + */ + fun setLocale(localeTag: String?) +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index 183761e9ba..d4ece5baf1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -1,10 +1,15 @@ package io.github.sds100.keymapper.base.settings +import android.os.Build +import androidx.annotation.RequiresApi import androidx.datastore.preferences.core.Preferences +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.sound.SoundFileInfo import io.github.sds100.keymapper.base.actions.sound.SoundsManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.base.system.inputmethod.SwitchImeInterface +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult @@ -30,6 +35,7 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map +@ViewModelScoped class ConfigSettingsUseCaseImpl @Inject constructor( private val preferences: PreferenceRepository, private val permissionAdapter: PermissionAdapter, @@ -42,6 +48,8 @@ class ConfigSettingsUseCaseImpl @Inject constructor( private val devicesAdapter: DevicesAdapter, private val buildConfigProvider: BuildConfigProvider, private val notificationAdapter: NotificationAdapter, + private val appLocaleAdapter: AppLocaleAdapter, + private val resourceProvider: ResourceProvider, ) : ConfigSettingsUseCase { private val imeHelper by lazy { @@ -194,6 +202,20 @@ class ConfigSettingsUseCaseImpl @Inject constructor( override fun openNotificationChannelSettings(channelId: String) { notificationAdapter.openChannelSettings(channelId) } + + override val currentLocaleDisplayName: StateFlow = + appLocaleAdapter.currentLocaleDisplayName + + override fun launchAppLocaleSettingsScreen(): Boolean = + appLocaleAdapter.launchAppLocaleSettingsScreen() + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun getSupportedLocales(): List = + appLocaleAdapter.getSupportedLocales() + + override fun setLocale(localeTag: String?) { + appLocaleAdapter.setLocale(localeTag) + } } interface ConfigSettingsUseCase { @@ -239,4 +261,13 @@ interface ConfigSettingsUseCase { val connectedInputDevices: StateFlow>> fun resetAllSettings() + + // Locale settings (Android 13+ only) + val currentLocaleDisplayName: StateFlow + fun launchAppLocaleSettingsScreen(): Boolean + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + fun getSupportedLocales(): List + + fun setLocale(localeTag: String?) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 9cbcb0ed7a..625475e0b9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material.icons.outlined.Android import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.FindInPage import androidx.compose.material.icons.outlined.Gamepad +import androidx.compose.material.icons.outlined.Language import androidx.compose.material.icons.outlined.OfflineBolt import androidx.compose.material.icons.rounded.Code import androidx.compose.material.icons.rounded.Construction @@ -78,10 +79,12 @@ import kotlinx.coroutines.launch private val isExpertModeSupported = Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API private val isAutoSwitchImeSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R +private val isLanguageSettingsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU @Composable fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { val state by viewModel.mainScreenState.collectAsStateWithLifecycle() + val currentLocaleDisplayName by viewModel.currentLocaleDisplayName.collectAsStateWithLifecycle() val snackbarHostState = SnackbarHostState() var showAutomaticBackupDialog by remember { mutableStateOf(false) } val context = LocalContext.current @@ -146,6 +149,8 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) ) { Content( state = state, + currentLocaleDisplayName = currentLocaleDisplayName ?: "", + onLanguageClick = viewModel::onLanguageClick, onThemeSelected = viewModel::onThemeSelected, onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick, onDefaultOptionsClick = viewModel::onDefaultOptionsClick, @@ -241,6 +246,8 @@ private fun SettingsScreen( private fun Content( modifier: Modifier = Modifier, state: MainSettingsState, + currentLocaleDisplayName: String = "", + onLanguageClick: () -> Unit = { }, onThemeSelected: (Theme) -> Unit = { }, onPauseResumeNotificationClick: () -> Unit = { }, onDefaultOptionsClick: () -> Unit = { }, @@ -287,6 +294,21 @@ private fun Content( onStateSelected = onThemeSelected, ) + OptionPageButton( + title = stringResource(R.string.title_pref_language), + text = if (isLanguageSettingsSupported) { + currentLocaleDisplayName + } else { + stringResource( + R.string.error_sdk_version_too_low, + BuildUtils.getSdkVersionName(Build.VERSION_CODES.TIRAMISU), + ) + }, + icon = Icons.Outlined.Language, + onClick = onLanguageClick, + enabled = isLanguageSettingsSupported, + ) + OptionPageButton( title = stringResource(R.string.title_pref_show_toggle_keymaps_notification), text = stringResource(R.string.summary_pref_show_toggle_keymaps_notification), @@ -472,6 +494,7 @@ private fun Preview() { SettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { Content( state = MainSettingsState(), + currentLocaleDisplayName = "System default", ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 7628eb1edc..1fd60bb3bf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -22,7 +22,6 @@ import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults -import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( @@ -110,6 +110,17 @@ class SettingsViewModel @Inject constructor( ) }.stateIn(viewModelScope, SharingStarted.Lazily, AutomaticChangeImeSettingsState()) + /** + * The display name of the currently selected locale, or null if not supported (Android < 13). + */ + val currentLocaleDisplayName: StateFlow = useCase.currentLocaleDisplayName + + fun onLanguageClick() { + viewModelScope.launch { + useCase.launchAppLocaleSettingsScreen() + } + } + fun setAutomaticBackupLocation(uri: String) = useCase.setAutomaticBackupLocation(uri) fun disableAutomaticBackup() = useCase.disableAutomaticBackup() @@ -167,10 +178,6 @@ class SettingsViewModel @Inject constructor( } } - fun onRequestRootClick() { - useCase.requestRootPermission() - } - fun onExpertModeClick() { viewModelScope.launch { navigate("expert_mode_settings", NavDestination.ExpertMode) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 1e5a335be7..90b5ba20ad 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -692,6 +692,11 @@ Light Dark System + + Language + Choose the app language + System default + diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt index c38e7e074f..3dfd55b876 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt @@ -27,8 +27,11 @@ object SettingsUtils { return try { when (T::class) { Int::class -> Settings.System.getInt(contentResolver, name) as T? + String::class -> Settings.System.getString(contentResolver, name) as T? + Float::class -> Settings.System.getFloat(contentResolver, name) as T? + Long::class -> Settings.System.getLong(contentResolver, name) as T? else -> { @@ -49,8 +52,11 @@ object SettingsUtils { return try { when (T::class) { Int::class -> Settings.Secure.getInt(contentResolver, name) as T? + String::class -> Settings.Secure.getString(contentResolver, name) as T? + Float::class -> Settings.Secure.getFloat(contentResolver, name) as T? + Long::class -> Settings.Secure.getLong(contentResolver, name) as T? else -> { @@ -71,8 +77,11 @@ object SettingsUtils { return try { when (T::class) { Int::class -> Settings.Global.getInt(contentResolver, name) as T? + String::class -> Settings.Global.getString(contentResolver, name) as T? + Float::class -> Settings.Global.getFloat(contentResolver, name) as T? + Long::class -> Settings.Global.getLong(contentResolver, name) as T? else -> { @@ -93,8 +102,11 @@ object SettingsUtils { return when (T::class) { Int::class -> Settings.System.putInt(contentResolver, name, value as Int) + String::class -> Settings.System.putString(contentResolver, name, value as String) + Float::class -> Settings.System.putFloat(contentResolver, name, value as Float) + Long::class -> Settings.System.putLong(contentResolver, name, value as Long) else -> { @@ -112,8 +124,11 @@ object SettingsUtils { return when (T::class) { Int::class -> Settings.Secure.putInt(contentResolver, name, value as Int) + String::class -> Settings.Secure.putString(contentResolver, name, value as String) + Float::class -> Settings.Secure.putFloat(contentResolver, name, value as Float) + Long::class -> Settings.Secure.putLong(contentResolver, name, value as Long) else -> { @@ -131,8 +146,11 @@ object SettingsUtils { return when (T::class) { Int::class -> Settings.Global.putInt(contentResolver, name, value as Int) + String::class -> Settings.Global.putString(contentResolver, name, value as String) + Float::class -> Settings.Global.putFloat(contentResolver, name, value as Float) + Long::class -> Settings.Global.putLong(contentResolver, name, value as Long) else -> { @@ -141,8 +159,13 @@ object SettingsUtils { } } - fun launchSettingsScreen(ctx: Context, action: String, fragmentArg: String? = null) { - val intent = Intent(action).apply { + fun launchSettingsScreen( + ctx: Context, + action: String, + uri: Uri? = null, + fragmentArg: String? = null, + ): Boolean { + val intent = Intent(action, uri).apply { if (fragmentArg != null) { val fragmentArgKey = ":settings:fragment_args_key" val showFragmentArgsKey = ":settings:show_fragment_args" @@ -161,8 +184,10 @@ object SettingsUtils { try { ctx.startActivity(intent) + return true } catch (e: ActivityNotFoundException) { Timber.e("Failed to start Settings activity: $e") + return false } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index a5c3f66e45..a6ba627e9b 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -289,7 +289,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( SettingsUtils.launchSettingsScreen( ctx, Settings.ACTION_DEVICE_INFO_SETTINGS, - "build_number", + fragmentArg = "build_number", ) coroutineScope.launch { @@ -380,7 +380,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( SettingsUtils.launchSettingsScreen( ctx, Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS, - "toggle_adb_wireless", + fragmentArg = "toggle_adb_wireless", ) } From 9a10bf8d4dbcb5315b66743de85036fd9f2fa91b Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 13:18:17 +0100 Subject: [PATCH 21/48] add issue links to changelog --- CHANGELOG.md | 662 +++++++++++++++++++++++++-------------------------- 1 file changed, 331 insertions(+), 331 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7cffe8079..7aa8fc01f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,17 +4,17 @@ ## Added -- #1970 dynamically build the key code list so key codes in new Android releases are automatically +- [#1970](https://github.com/keymapperorg/KeyMapper/issues/1970) dynamically build the key code list so key codes in new Android releases are automatically included. -- #1939 show notification when Expert Mode fails to start due to be being disconnected from WiFi. -- #1973 add setting to change app language on Android 13+. +- [#1939](https://github.com/keymapperorg/KeyMapper/issues/1939) show notification when Expert Mode fails to start due to be being disconnected from WiFi. +- [#1973](https://github.com/keymapperorg/KeyMapper/issues/1973) add setting to change app language on Android 13+. ## Fixed -- #1986 trigger screen is usable on slightly rectangular screens with a low DPI. -- #1972 Expert Mode works on Android 10. -- #1976 Panic in Rust system bridge code on some devices. -- #1971 Media actions work again in some apps, like YouTube. +- [#1986](https://github.com/keymapperorg/KeyMapper/issues/1986) trigger screen is usable on slightly rectangular screens with a low DPI. +- [#1972](https://github.com/keymapperorg/KeyMapper/issues/1972) Expert Mode works on Android 10. +- [#1976](https://github.com/keymapperorg/KeyMapper/issues/1976) Panic in Rust system bridge code on some devices. +- [#1971](https://github.com/keymapperorg/KeyMapper/issues/1971) Media actions work again in some apps, like YouTube. - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) @@ -23,14 +23,14 @@ ## Added -- #1964 show the command to start Expert Mode with a shell command. +- [#1964](https://github.com/keymapperorg/KeyMapper/issues/1964) show the command to start Expert Mode with a shell command. ## Bug fixes -- #1968 Device controls action no longer works on Android 16+ so it has been disabled on new Android +- [#1968](https://github.com/keymapperorg/KeyMapper/issues/1968) Device controls action no longer works on Android 16+ so it has been disabled on new Android versions. -- #1967 Still start system bridge if granting WRITE_SECURE_SETTINGS fails. -- #1965 Better system bridge support on Xiaomi devices and ask to enable "USB debugging security +- [#1967](https://github.com/keymapperorg/KeyMapper/issues/1967) Still start system bridge if granting WRITE_SECURE_SETTINGS fails. +- [#1965](https://github.com/keymapperorg/KeyMapper/issues/1965) Better system bridge support on Xiaomi devices and ask to enable "USB debugging security settings" in developer options. ## [4.0.0 Beta 5](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.05) @@ -41,12 +41,12 @@ Happy new year! ## Added -- #1947 show tip to use expert mode where the old option for screen off remapping used to be +- [#1947](https://github.com/keymapperorg/KeyMapper/issues/1947) show tip to use expert mode where the old option for screen off remapping used to be ## Bug fixes -- #1955 step forward and step backward media actions support more apps. -- #1940 improve reliability of clicking pairing code button in Wireless Debugging settings. +- [#1955](https://github.com/keymapperorg/KeyMapper/issues/1955) step forward and step backward media actions support more apps. +- [#1940](https://github.com/keymapperorg/KeyMapper/issues/1940) improve reliability of clicking pairing code button in Wireless Debugging settings. ## [4.0.0 Beta 4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.04) @@ -59,21 +59,21 @@ free. ## Added -- #1915 ask user to remove "adb shell" from Shell command. -- #1904 inform the user how to enable the accessibility service with PRO mode or ADB. -- #1911 constraint for physical device orientation that ignores auto rotate setting. -- #1918 improve how key event actions are performed with system bridge. -- #1905 system bridge log is now visible in Key Mapper log. -- #1941 show loading indicator when starting system bridge. +- [#1915](https://github.com/keymapperorg/KeyMapper/issues/1915) ask user to remove "adb shell" from Shell command. +- [#1904](https://github.com/keymapperorg/KeyMapper/issues/1904) inform the user how to enable the accessibility service with PRO mode or ADB. +- [#1911](https://github.com/keymapperorg/KeyMapper/issues/1911) constraint for physical device orientation that ignores auto rotate setting. +- [#1918](https://github.com/keymapperorg/KeyMapper/issues/1918) improve how key event actions are performed with system bridge. +- [#1905](https://github.com/keymapperorg/KeyMapper/issues/1905) system bridge log is now visible in Key Mapper log. +- [#1941](https://github.com/keymapperorg/KeyMapper/issues/1941) show loading indicator when starting system bridge. ## Bug fixes -- #1913 actually save the option to detect with scan code -- #1931 fix Close and Remove From Recents action on some Android 13 revisions -- #1926 PRO mode triggers for external devices work when the device reconnects. -- #1918 PRO mode key maps can input key codes that aren't originally supported by the trigger +- [#1913](https://github.com/keymapperorg/KeyMapper/issues/1913) actually save the option to detect with scan code +- [#1931](https://github.com/keymapperorg/KeyMapper/issues/1931) fix Close and Remove From Recents action on some Android 13 revisions +- [#1926](https://github.com/keymapperorg/KeyMapper/issues/1926) PRO mode triggers for external devices work when the device reconnects. +- [#1918](https://github.com/keymapperorg/KeyMapper/issues/1918) PRO mode key maps can input key codes that aren't originally supported by the trigger device. -- #1934 hold down option for Tap Screen action is added back. +- [#1934](https://github.com/keymapperorg/KeyMapper/issues/1934) hold down option for Tap Screen action is added back. - Log less verbose. ## [4.0.0 Beta 3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.03) @@ -82,18 +82,18 @@ free. ## Added -- #1871 action to modify any system settings. -- #1221 action to show a custom notification. -- #1491 action to toggle/enable/disable hotspot. -- #1414 constraint for when the keyboard is showing. -- #1900 log to logcat if extra logging is enabled. -- #1902 add toggle next to record trigger button to use PRO mode. -- #1909 categorise constraints similar to actions. +- [#1871](https://github.com/keymapperorg/KeyMapper/issues/1871) action to modify any system settings. +- [#1221](https://github.com/keymapperorg/KeyMapper/issues/1221) action to show a custom notification. +- [#1491](https://github.com/keymapperorg/KeyMapper/issues/1491) action to toggle/enable/disable hotspot. +- [#1414](https://github.com/keymapperorg/KeyMapper/issues/1414) constraint for when the keyboard is showing. +- [#1900](https://github.com/keymapperorg/KeyMapper/issues/1900) log to logcat if extra logging is enabled. +- [#1902](https://github.com/keymapperorg/KeyMapper/issues/1902) add toggle next to record trigger button to use PRO mode. +- [#1909](https://github.com/keymapperorg/KeyMapper/issues/1909) categorise constraints similar to actions. ## Bug fixes -- #1901 prompt user to set default USB configuration to 'No data transfer' after starting pro mode. -- #1898 do not launch directly into the Wireless Debugging activity on Xiaomi devices due to a bug +- [#1901](https://github.com/keymapperorg/KeyMapper/issues/1901) prompt user to set default USB configuration to 'No data transfer' after starting pro mode. +- [#1898](https://github.com/keymapperorg/KeyMapper/issues/1898) do not launch directly into the Wireless Debugging activity on Xiaomi devices due to a bug they introduced. ## [4.0.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.02) @@ -102,7 +102,7 @@ free. ## Added -- #1890 add button to save log to file and share it. The clipboard button now cuts off older entries +- [#1890](https://github.com/keymapperorg/KeyMapper/issues/1890) add button to save log to file and share it. The clipboard button now cuts off older entries and keeps newest ones. ## Fixed @@ -111,7 +111,7 @@ free. method with Wireless Debugging and WRITE_SECURE_SETTINGS permission. - Starting system bridge for the first time would be janky because granting READ_LOGS kills the app process. Only grant for READ_LOGS when sharing logcat from settings. -- #1886 mobile data actions work in PRO mode. +- [#1886](https://github.com/keymapperorg/KeyMapper/issues/1886) mobile data actions work in PRO mode. ## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) @@ -119,22 +119,22 @@ free. ## Added -- #761 Detect keys with scancodes. Key Mapper will do this automatically if the key code is unknown +- [#761](https://github.com/keymapperorg/KeyMapper/issues/761) Detect keys with scancodes. Key Mapper will do this automatically if the key code is unknown or you record different physical keys from the same device with the same key code. - Redesign the Settings screen. - Shortcuts on the trigger screen that guide you how to set up different types of buttons. -- #1788 dismiss lockscreen when launching app action from lockscreen +- [#1788](https://github.com/keymapperorg/KeyMapper/issues/1788) dismiss lockscreen when launching app action from lockscreen - Show tips for parallel and sequence triggers, and constraints in the trigger screen -- #397 enable/disable all key maps in a group -- #1773 Option to show floating buttons on top of keyboard or notification panel. -- #1335 Intent API to enable/disable/toggle a key map. -- #114 action to force stop app, and an action to clear an app from recents -- #727 Actions to send SMS messages: "Send SMS" and "Compose SMS" -- #1819 Explain how to enable the accessibility service restricted setting -- #661 Action to execute shell commands. -- #991 Consolidated volume and stream actions. -- #1066 Action to mute/unmute microphone. -- #985 Constraints for foldable hinge being open/closed. +- [#397](https://github.com/keymapperorg/KeyMapper/issues/397) enable/disable all key maps in a group +- [#1773](https://github.com/keymapperorg/KeyMapper/issues/1773) Option to show floating buttons on top of keyboard or notification panel. +- [#1335](https://github.com/keymapperorg/KeyMapper/issues/1335) Intent API to enable/disable/toggle a key map. +- [#114](https://github.com/keymapperorg/KeyMapper/issues/114) action to force stop app, and an action to clear an app from recents +- [#727](https://github.com/keymapperorg/KeyMapper/issues/727) Actions to send SMS messages: "Send SMS" and "Compose SMS" +- [#1819](https://github.com/keymapperorg/KeyMapper/issues/1819) Explain how to enable the accessibility service restricted setting +- [#661](https://github.com/keymapperorg/KeyMapper/issues/661) Action to execute shell commands. +- [#991](https://github.com/keymapperorg/KeyMapper/issues/991) Consolidated volume and stream actions. +- [#1066](https://github.com/keymapperorg/KeyMapper/issues/1066) Action to mute/unmute microphone. +- [#985](https://github.com/keymapperorg/KeyMapper/issues/985) Constraints for foldable hinge being open/closed. ## Removed @@ -152,16 +152,16 @@ free. - Restoring subgroups works and does not freeze Key Mapper. - Do not show duplicate constraint shortcuts. - Make WiFi connected constraints more reliable -- #1818 auto switching of the Key Mapper keyboard when typing is more reliable and quicker on +- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) auto switching of the Key Mapper keyboard when typing is more reliable and quicker on Android 13+ -- #1818 auto switching of the Key Mapper keyboard now requires Android 11+. On older versions it was +- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) auto switching of the Key Mapper keyboard now requires Android 11+. On older versions it was only possible with WRITE_SECURE_SETTINGS but very few users are on these old Android versions so it is not worth the extra maintenance effort. -- #1818 the Key Mapper GUI Keyboard is no longer mentioned in the app. It still works but PRO mode +- [#1818](https://github.com/keymapperorg/KeyMapper/issues/1818) the Key Mapper GUI Keyboard is no longer mentioned in the app. It still works but PRO mode and the auto switching feature are the preferred way to work around the limitations of the Key Mapper keyboard. - Allow selecting notification and alarm sound and not just ringtones for Sound action. -- #1064 wait for switch keyboard action to complete before doing next action. +- [#1064](https://github.com/keymapperorg/KeyMapper/issues/1064) wait for switch keyboard action to complete before doing next action. ## [3.2.1](https://github.com/sds100/KeyMapper/releases/tag/v3.2.1) @@ -177,33 +177,33 @@ free. ## Added -- #1466 show onboarding when creating a key map for the first time -- #1729 target Android 16. -- #1725 action to move cursor to previous/next character, word, line, paragraph, or page. +- [#1466](https://github.com/keymapperorg/KeyMapper/issues/1466) show onboarding when creating a key map for the first time +- [#1729](https://github.com/keymapperorg/KeyMapper/issues/1729) target Android 16. +- [#1725](https://github.com/keymapperorg/KeyMapper/issues/1725) action to move cursor to previous/next character, word, line, paragraph, or page. - Names for new key codes introduced in recent Android versions ## Changed -- #1711 major refactoring of the entire codebase into separate Gradle modules. -- #1701 improve the order of the actions and categories +- [#1711](https://github.com/keymapperorg/KeyMapper/issues/1711) major refactoring of the entire codebase into separate Gradle modules. +- [#1701](https://github.com/keymapperorg/KeyMapper/issues/1701) improve the order of the actions and categories - Key Mapper keyboard or Shizuku are no longer required for the action to move the cursor to the end ## Bug fixes -- #1686 (more fixes) do not show some screens behind system bars on the left/right side of the +- [#1686](https://github.com/keymapperorg/KeyMapper/issues/1686) (more fixes) do not show some screens behind system bars on the left/right side of the device. -- #1701 optimize the trigger screen for smaller screens so elements are less cut off. -- #1699 Do not highlight a floating button as if it is pressed after triggering a key event action +- [#1701](https://github.com/keymapperorg/KeyMapper/issues/1701) optimize the trigger screen for smaller screens so elements are less cut off. +- [#1699](https://github.com/keymapperorg/KeyMapper/issues/1699) Do not highlight a floating button as if it is pressed after triggering a key event action from it. - Button to copy the key map UID to the clipboard is invisible on small screens. -- #1709 Quick settings tiles were causing crashes on Android 15 -- #1714 Editing "interact with app element" actions works. -- #1707 do not back up sound files if no key maps are using them -- #797 #1719 execute key maps that can fix themselves. E.g having an action to select the Key Mapper +- [#1709](https://github.com/keymapperorg/KeyMapper/issues/1709) Quick settings tiles were causing crashes on Android 15 +- [#1714](https://github.com/keymapperorg/KeyMapper/issues/1714) Editing "interact with app element" actions works. +- [#1707](https://github.com/keymapperorg/KeyMapper/issues/1707) do not back up sound files if no key maps are using them +- [#797](https://github.com/keymapperorg/KeyMapper/issues/797) [#1719](https://github.com/keymapperorg/KeyMapper/issues/1719) execute key maps that can fix themselves. E.g having an action to select the Key Mapper Keyboard before a key code action. -- #1735 Floating buttons no longer flash on screen when the accessibility service restarts if they +- [#1735](https://github.com/keymapperorg/KeyMapper/issues/1735) Floating buttons no longer flash on screen when the accessibility service restarts if they are not supposed to be visible. -- #1717 do not show floating buttons if quick settings is expanded on the lockscreen. +- [#1717](https://github.com/keymapperorg/KeyMapper/issues/1717) do not show floating buttons if quick settings is expanded on the lockscreen. - Correctly show error that Airplane mode actions require root ## [3.1.1](https://github.com/sds100/KeyMapper/releases/tag/v3.1.1) @@ -212,13 +212,13 @@ free. ## Added -- #1637 show a home screen error if notification permission is not granted. -- #1435 Pick system sounds/ringtones for the Sound action. +- [#1637](https://github.com/keymapperorg/KeyMapper/issues/1637) show a home screen error if notification permission is not granted. +- [#1435](https://github.com/keymapperorg/KeyMapper/issues/1435) Pick system sounds/ringtones for the Sound action. ## Bug fixes - Do not automatically select the key mapper keyboard when the accessibility service starts. -- #1686 do not show some screens behind system bars on the left/right side of the device. +- [#1686](https://github.com/keymapperorg/KeyMapper/issues/1686) do not show some screens behind system bars on the left/right side of the device. - Use same sized list items when choosing a constraint. ## [3.1.0](https://github.com/sds100/KeyMapper/releases/tag/v3.1.0) @@ -227,10 +227,10 @@ free. ## Added -- #699 Time constraints ⏰ -- #257 Action to interact with user interface elements inside other apps. -- #1663 Actions to stop, step forward, and step backward playing media. -- #1682 Show "Purchased!" text next to the use button for advanced triggers. +- [#699](https://github.com/keymapperorg/KeyMapper/issues/699) Time constraints ⏰ +- [#257](https://github.com/keymapperorg/KeyMapper/issues/257) Action to interact with user interface elements inside other apps. +- [#1663](https://github.com/keymapperorg/KeyMapper/issues/1663) Actions to stop, step forward, and step backward playing media. +- [#1682](https://github.com/keymapperorg/KeyMapper/issues/1682) Show "Purchased!" text next to the use button for advanced triggers. ## Changed @@ -238,11 +238,11 @@ free. ## Bug fixes -- #1683 key event actions work in Minecraft and other apps again. +- [#1683](https://github.com/keymapperorg/KeyMapper/issues/1683) key event actions work in Minecraft and other apps again. - Export log files as .txt instead of .zip files. -- #1684 Removed the redundant and broken refresh devices button when configuring a key event action +- [#1684](https://github.com/keymapperorg/KeyMapper/issues/1684) Removed the redundant and broken refresh devices button when configuring a key event action because they are automatically refreshed anyway. -- #1687 restoring key map groups would sometimes fail. +- [#1687](https://github.com/keymapperorg/KeyMapper/issues/1687) restoring key map groups would sometimes fail. ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) @@ -250,31 +250,31 @@ free. ## Added -- #1652 Bring back the menu button to show input method picker. -- #1657 Turn on repeat by default for volume actions. +- [#1652](https://github.com/keymapperorg/KeyMapper/issues/1652) Bring back the menu button to show input method picker. +- [#1657](https://github.com/keymapperorg/KeyMapper/issues/1657) Turn on repeat by default for volume actions. ## Changed -- #1654 The Key Mapper keyboard is now required again for Text actions because the accessibility +- [#1654](https://github.com/keymapperorg/KeyMapper/issues/1654) The Key Mapper keyboard is now required again for Text actions because the accessibility service API does not work in all situations. -- #1653 Hide the export/import menu buttons in groups. -- #1553 Hide double press option for side key and fingerprint gesture triggers because it is +- [#1653](https://github.com/keymapperorg/KeyMapper/issues/1653) Hide the export/import menu buttons in groups. +- [#1553](https://github.com/keymapperorg/KeyMapper/issues/1553) Hide double press option for side key and fingerprint gesture triggers because it is misleading. Double activations can be done with sequence triggers instead. -- #1669 Change quick settings tile text. +- [#1669](https://github.com/keymapperorg/KeyMapper/issues/1669) Change quick settings tile text. ## Bug fixes - Inputting key events with Shizuku does not crash the app if a Key Mapper keyboard is being used at the same time. And latency when inputting key events has been improved in some apps. -- #1646 disabling Bluetooth clears the list of connected devices. -- #1655 do not crash when restoring key map groups. -- #1649 show purchase verification failed error if no network connection. -- #1648 caching purchases works so you can use floating buttons and assistant trigger without an +- [#1646](https://github.com/keymapperorg/KeyMapper/issues/1646) disabling Bluetooth clears the list of connected devices. +- [#1655](https://github.com/keymapperorg/KeyMapper/issues/1655) do not crash when restoring key map groups. +- [#1649](https://github.com/keymapperorg/KeyMapper/issues/1649) show purchase verification failed error if no network connection. +- [#1648](https://github.com/keymapperorg/KeyMapper/issues/1648) caching purchases works so you can use floating buttons and assistant trigger without an internet connection. -- #1658 floating buttons appear in the wrong place in portrait if saved in landscape. -- #1659 Use trigger does not work if the screen orientation changes when re-entering the app. -- #1668 Crashes when floating menu does not fit in the display height. -- #1667 Hold down mode UI is missing from 2.8. +- [#1658](https://github.com/keymapperorg/KeyMapper/issues/1658) floating buttons appear in the wrong place in portrait if saved in landscape. +- [#1659](https://github.com/keymapperorg/KeyMapper/issues/1659) Use trigger does not work if the screen orientation changes when re-entering the app. +- [#1668](https://github.com/keymapperorg/KeyMapper/issues/1668) Crashes when floating menu does not fit in the display height. +- [#1667](https://github.com/keymapperorg/KeyMapper/issues/1667) Hold down mode UI is missing from 2.8. ## [3.0.0](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0) @@ -282,7 +282,7 @@ _See the changes from previous 3.0 Beta releases._ #### 10 April 2025 -- #1635 do not crash if the URL for the HTTP action is malformed +- [#1635](https://github.com/keymapperorg/KeyMapper/issues/1635) do not crash if the URL for the HTTP action is malformed ## [3.0 Beta 5](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.5) @@ -290,11 +290,11 @@ _See the changes from previous 3.0 Beta releases as well._ #### 6 April 2025 -- #1625 HTTP Request action. +- [#1625](https://github.com/keymapperorg/KeyMapper/issues/1625) HTTP Request action. ## Bug fixes -- #1627 open camera app action does not work when device is locked +- [#1627](https://github.com/keymapperorg/KeyMapper/issues/1627) open camera app action does not work when device is locked ## [3.0 Beta 4](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.4) @@ -304,8 +304,8 @@ _See the changes from previous 3.0 Beta releases as well._ ## Added -- #1620 enable Key Mapper Basic Input Method without user interaction on Android 13+. -- #1619 Automatically select the non key mapper keyboard when the device is locked and wanting to +- [#1620](https://github.com/keymapperorg/KeyMapper/issues/1620) enable Key Mapper Basic Input Method without user interaction on Android 13+. +- [#1619](https://github.com/keymapperorg/KeyMapper/issues/1619) Automatically select the non key mapper keyboard when the device is locked and wanting to type. ## Changed @@ -314,7 +314,7 @@ _See the changes from previous 3.0 Beta releases as well._ ## Bug fixes -- #1618, #1532, #1590 The Key Mapper keyboard is no longer required for Text actions. +- [#1618](https://github.com/keymapperorg/KeyMapper/issues/1618), [#1532](https://github.com/keymapperorg/KeyMapper/issues/1532), [#1590](https://github.com/keymapperorg/KeyMapper/issues/1590) The Key Mapper keyboard is no longer required for Text actions. - Flashlight action works again on devices that do not support variable brightness ## [3.0 Beta 3](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.3) @@ -327,10 +327,10 @@ This is not an April Fool's joke ;) ## Added -- #320 🗂️ Key map groups! You can now sort key maps into groups and share constraints across all the +- [#320](https://github.com/keymapperorg/KeyMapper/issues/320) 🗂️ Key map groups! You can now sort key maps into groups and share constraints across all the key maps in the group. -- #1586 🎨 Customise floating button border and background opacity. -- #1276 Use key event scan code as fallback if the key code is unrecognized. +- [#1586](https://github.com/keymapperorg/KeyMapper/issues/1586) 🎨 Customise floating button border and background opacity. +- [#1276](https://github.com/keymapperorg/KeyMapper/issues/1276) Use key event scan code as fallback if the key code is unrecognized. - Make it clearer that the instructions need to be read for the assistant trigger. ## Changed @@ -344,8 +344,8 @@ This is not an April Fool's joke ;) the status bar. - Do not show floating buttons on the always-on display or when the display is "off". - Prompt to unlock device when tapping "Go back" on the floating menu. -- #1596 Do not show the option for front flashlight if the device does not have one. -- #1598 Do not allow changing flashlight brightness on devices that do not support it. +- [#1596](https://github.com/keymapperorg/KeyMapper/issues/1596) Do not show the option for front flashlight if the device does not have one. +- [#1598](https://github.com/keymapperorg/KeyMapper/issues/1598) Do not allow changing flashlight brightness on devices that do not support it. - Omit "Back" from Back flashlight actions and constraints since most devices only have a back flashlight anyway. - Do not ask for which flashlight to use in constraints if the device only has one @@ -356,21 +356,21 @@ This is not an April Fool's joke ;) ## Added -- #1560 Action to change flashlight brightness and also set a custom brightness when enabling the +- [#1560](https://github.com/keymapperorg/KeyMapper/issues/1560) Action to change flashlight brightness and also set a custom brightness when enabling the flashlight. - Prompt to unlock device when using a floating button as a trigger from the lock screen ## Changed -- #1577 Move unsupported actions to the bottom of the list and do not allow selecting root actions +- [#1577](https://github.com/keymapperorg/KeyMapper/issues/1577) Move unsupported actions to the bottom of the list and do not allow selecting root actions if root permission is not granted. -- #1593 Deprecate the 'Open menu' action by not letting new key maps use it. It is a relic of the +- [#1593](https://github.com/keymapperorg/KeyMapper/issues/1593) Deprecate the 'Open menu' action by not letting new key maps use it. It is a relic of the past when most apps had a 3-dot menu with a consistent content description making it somewhat easy to identify. ## Bug fixes -- #1585 Track changes when editing key maps and only prompt to discard changes if there were indeed +- [#1585](https://github.com/keymapperorg/KeyMapper/issues/1585) Track changes when editing key maps and only prompt to discard changes if there were indeed changes. ## [3.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.1) @@ -382,26 +382,26 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Added -- #1407 New trigger! Add floating buttons on top of other apps to input key maps. +- [#1407](https://github.com/keymapperorg/KeyMapper/issues/1407) New trigger! Add floating buttons on top of other apps to input key maps. - Key maps are much more dense on the home screen. - Button to pause/resume key maps at the top of the home screen. -- #1502 Constraint for lockscreen is (not) showing. -- #1203 Show a share sheet after exporting key maps rather than asking where to store it. This +- [#1502](https://github.com/keymapperorg/KeyMapper/issues/1502) Constraint for lockscreen is (not) showing. +- [#1203](https://github.com/keymapperorg/KeyMapper/issues/1203) Show a share sheet after exporting key maps rather than asking where to store it. This solves the problem when no apps are installed to select where to back it up. You can still find the file in the Downloads file. -- #1531 Show shortcuts to quickly add recently used actions and constraints. -- #1487 Add confirmation dialog when importing key maps and offer the option to replace all the key +- [#1531](https://github.com/keymapperorg/KeyMapper/issues/1531) Show shortcuts to quickly add recently used actions and constraints. +- [#1487](https://github.com/keymapperorg/KeyMapper/issues/1487) Add confirmation dialog when importing key maps and offer the option to replace all the key maps or append to the list. -- #1546 Add short explanation of what constraints mean on top of the list. -- #1548 Dynamically change key map enabled switch label. -- #1562 Import key maps by opening .json and .zip files from other apps and file managers. +- [#1546](https://github.com/keymapperorg/KeyMapper/issues/1546) Add short explanation of what constraints mean on top of the list. +- [#1548](https://github.com/keymapperorg/KeyMapper/issues/1548) Dynamically change key map enabled switch label. +- [#1562](https://github.com/keymapperorg/KeyMapper/issues/1562) Import key maps by opening .json and .zip files from other apps and file managers. ## Bug fixes -- #1518 detect more apps that are playing media (fix to previous fix). -- #1545 support phone call constraints in more apps. -- #1536 'Edit action' sometimes does not appear. -- #1507 only vibrate once when mixing short, long, and double press key maps. +- [#1518](https://github.com/keymapperorg/KeyMapper/issues/1518) detect more apps that are playing media (fix to previous fix). +- [#1545](https://github.com/keymapperorg/KeyMapper/issues/1545) support phone call constraints in more apps. +- [#1536](https://github.com/keymapperorg/KeyMapper/issues/1536) 'Edit action' sometimes does not appear. +- [#1507](https://github.com/keymapperorg/KeyMapper/issues/1507) only vibrate once when mixing short, long, and double press key maps. - Prevent various system errors from crashing the apps. ## [2.8.3](https://github.com/sds100/KeyMapper/releases/tag/v2.8.3) @@ -410,12 +410,12 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Changed -- #1474 always allow specifying a name for key map launcher shortcuts. -- #1533 simplify naming of ringer mode actions. +- [#1474](https://github.com/keymapperorg/KeyMapper/issues/1474) always allow specifying a name for key map launcher shortcuts. +- [#1533](https://github.com/keymapperorg/KeyMapper/issues/1533) simplify naming of ringer mode actions. ## Bug fixes -- #1535 side key/assistant trigger does not trigger from non-assistant buttons. +- [#1535](https://github.com/keymapperorg/KeyMapper/issues/1535) side key/assistant trigger does not trigger from non-assistant buttons. ## [2.8.2](https://github.com/sds100/KeyMapper/releases/tag/v2.8.2) @@ -423,13 +423,13 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Changes -- #1514, #1454 Improving naming of assistant trigger to also refer to the side key and do not force +- [#1514](https://github.com/keymapperorg/KeyMapper/issues/1514), [#1454](https://github.com/keymapperorg/KeyMapper/issues/1454) Improving naming of assistant trigger to also refer to the side key and do not force the user to select Key Mapper as the assistant. ## Bug fixes -- #1461 fix: crash on startup due to getting MotionEvent device -- #1518 fix: detect apps playing media without a notification for media constraints +- [#1461](https://github.com/keymapperorg/KeyMapper/issues/1461) fix: crash on startup due to getting MotionEvent device +- [#1518](https://github.com/keymapperorg/KeyMapper/issues/1518) fix: detect apps playing media without a notification for media constraints ## [2.8.1](https://github.com/sds100/KeyMapper/releases/tag/v2.8.1) @@ -437,14 +437,14 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Bug fixes -- #1433 open Key Mapper by default and not the Assistant Trigger app. -- #1386 wait for sequence trigger timeout before triggering other overlapping triggers. -- #1449 improve the key mapper crashed dialog. -- #1415 make the discard changes dialog less confusing. -- #1440 do not show the "Button not detected?" bottom sheet every time you open the config key map +- [#1433](https://github.com/keymapperorg/KeyMapper/issues/1433) open Key Mapper by default and not the Assistant Trigger app. +- [#1386](https://github.com/keymapperorg/KeyMapper/issues/1386) wait for sequence trigger timeout before triggering other overlapping triggers. +- [#1449](https://github.com/keymapperorg/KeyMapper/issues/1449) improve the key mapper crashed dialog. +- [#1415](https://github.com/keymapperorg/KeyMapper/issues/1415) make the discard changes dialog less confusing. +- [#1440](https://github.com/keymapperorg/KeyMapper/issues/1440) do not show the "Button not detected?" bottom sheet every time you open the config key map screen in some cases. -- #1447 the app bar when configuring an Intent action would extend to the top of the screen. -- #1444 use the correct icon for screen on/off constraints. +- [#1447](https://github.com/keymapperorg/KeyMapper/issues/1447) the app bar when configuring an Intent action would extend to the top of the screen. +- [#1444](https://github.com/keymapperorg/KeyMapper/issues/1444) use the correct icon for screen on/off constraints. ## [2.8.0](https://github.com/sds100/KeyMapper/releases/tag/v2.8.0) @@ -452,30 +452,30 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Added -- #491 remap DPAD buttons. -- #1223 sort key maps by triggers, actions, constraints and options. -- #1344 target Android 15 and support edge-to-edge display mode. -- #1372 allow Shizuku features to work with Sui. -- #1391 button in Settings to reset all settings to their defaults. +- [#491](https://github.com/keymapperorg/KeyMapper/issues/491) remap DPAD buttons. +- [#1223](https://github.com/keymapperorg/KeyMapper/issues/1223) sort key maps by triggers, actions, constraints and options. +- [#1344](https://github.com/keymapperorg/KeyMapper/issues/1344) target Android 15 and support edge-to-edge display mode. +- [#1372](https://github.com/keymapperorg/KeyMapper/issues/1372) allow Shizuku features to work with Sui. +- [#1391](https://github.com/keymapperorg/KeyMapper/issues/1391) button in Settings to reset all settings to their defaults. ## Changed -- #1412 make the record trigger text clearer by saying it is recording. +- [#1412](https://github.com/keymapperorg/KeyMapper/issues/1412) make the record trigger text clearer by saying it is recording. ## Removed -- #1411 remove the app intro screen for remapping fingerprint gestures because almost all new phones +- [#1411](https://github.com/keymapperorg/KeyMapper/issues/1411) remove the app intro screen for remapping fingerprint gestures because almost all new phones do not support them anyway. ## Bug fixes -- #1426, #1434 key map launcher shortcut icons were white. -- #1410 vibrations not working on Android 13+. -- #1342 add missing Meta modifier options for key event actions. -- #1375 memory leak when rebinding to the relay service in the Key Mapper GUI Keyboard. -- #1376 Key Mapper Basic Input Method would not work on Android 14+ in some situations. -- #1094 wrong repository name in the introduction screen. -- #1387 some app shortcuts would not open on Android 14+. +- [#1426](https://github.com/keymapperorg/KeyMapper/issues/1426), [#1434](https://github.com/keymapperorg/KeyMapper/issues/1434) key map launcher shortcut icons were white. +- [#1410](https://github.com/keymapperorg/KeyMapper/issues/1410) vibrations not working on Android 13+. +- [#1342](https://github.com/keymapperorg/KeyMapper/issues/1342) add missing Meta modifier options for key event actions. +- [#1375](https://github.com/keymapperorg/KeyMapper/issues/1375) memory leak when rebinding to the relay service in the Key Mapper GUI Keyboard. +- [#1376](https://github.com/keymapperorg/KeyMapper/issues/1376) Key Mapper Basic Input Method would not work on Android 14+ in some situations. +- [#1094](https://github.com/keymapperorg/KeyMapper/issues/1094) wrong repository name in the introduction screen. +- [#1387](https://github.com/keymapperorg/KeyMapper/issues/1387) some app shortcuts would not open on Android 14+. ## [2.7.2](https://github.com/sds100/KeyMapper/releases/tag/v2.7.2) @@ -483,15 +483,15 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Added -- #1298 add action to launch the Android device controls screen for managing Home devices. +- [#1298](https://github.com/keymapperorg/KeyMapper/issues/1298) add action to launch the Android device controls screen for managing Home devices. ## Bug fixes -- #1342 add Meta modifier keys to the key event action. -- #1101 deprecate the toggle split screen action on Android 12L and newer. -- #1370 warn the user that extra permissions are required for the Launch app action on Xiaomi +- [#1342](https://github.com/keymapperorg/KeyMapper/issues/1342) add Meta modifier keys to the key event action. +- [#1101](https://github.com/keymapperorg/KeyMapper/issues/1101) deprecate the toggle split screen action on Android 12L and newer. +- [#1370](https://github.com/keymapperorg/KeyMapper/issues/1370) warn the user that extra permissions are required for the Launch app action on Xiaomi devices -- #1371 try to fix the app not opening on people's devices +- [#1371](https://github.com/keymapperorg/KeyMapper/issues/1371) try to fix the app not opening on people's devices ## [2.7.1](https://github.com/sds100/KeyMapper/releases/tag/v2.7.1) @@ -499,9 +499,9 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Bug fixes -- #1360 complete the documentation for advanced triggers at docs.keymapper.club. -- #1364 key event actions no longer crash when using Shizuku. -- #1362 backing up and restoring key maps works again. +- [#1360](https://github.com/keymapperorg/KeyMapper/issues/1360) complete the documentation for advanced triggers at docs.keymapper.club. +- [#1364](https://github.com/keymapperorg/KeyMapper/issues/1364) key event actions no longer crash when using Shizuku. +- [#1362](https://github.com/keymapperorg/KeyMapper/issues/1362) backing up and restoring key maps works again. ## [2.7.0](https://github.com/sds100/KeyMapper/releases/tag/v2.7.0) @@ -509,21 +509,21 @@ in Jetpack Compose, resulting in many improvements to the user experience. ## Added -- #1274 New trigger! You can now trigger your key maps from any of the ways your phone launches the +- [#1274](https://github.com/keymapperorg/KeyMapper/issues/1274) New trigger! You can now trigger your key maps from any of the ways your phone launches the assistant! This could be the Bixby button, Power button, or a button on your headset. -- #1304 Vietnamese translations. +- [#1304](https://github.com/keymapperorg/KeyMapper/issues/1304) Vietnamese translations. ## Bug fixes -- #1222 #1307 Key Mapper doesn't execute the correct app shortcut action if you created multiple +- [#1222](https://github.com/keymapperorg/KeyMapper/issues/1222) [#1307](https://github.com/keymapperorg/KeyMapper/issues/1307) Key Mapper doesn't execute the correct app shortcut action if you created multiple from the same app. -- #1328 Single-character non-ASCII TEXT_BLOCK input crashes the service +- [#1328](https://github.com/keymapperorg/KeyMapper/issues/1328) Single-character non-ASCII TEXT_BLOCK input crashes the service ## [2.6.2](https://github.com/sds100/KeyMapper/releases/tag/v2.6.2) #### 9 September 2024 -- #1293 Checkbox buttons were invisible when configuring some actions. +- [#1293](https://github.com/keymapperorg/KeyMapper/issues/1293) Checkbox buttons were invisible when configuring some actions. ## [2.6.1](https://github.com/sds100/KeyMapper/releases/tag/v2.6.1) @@ -533,29 +533,29 @@ This release adds support for Android 14 and fixes some bugs associated with it. ### Added -- #1256 Add Russian and Chinese Simplified translations. Update other languages. -- #1282 Add Assist key code as screen off trigger for Bixby button. +- [#1256](https://github.com/keymapperorg/KeyMapper/issues/1256) Add Russian and Chinese Simplified translations. Update other languages. +- [#1282](https://github.com/keymapperorg/KeyMapper/issues/1282) Add Assist key code as screen off trigger for Bixby button. ### Bug fixes -- #1218, #1251 Key event actions and triggering key maps from an intent were delayed by 1 second on +- [#1218](https://github.com/keymapperorg/KeyMapper/issues/1218), [#1251](https://github.com/keymapperorg/KeyMapper/issues/1251) Key event actions and triggering key maps from an intent were delayed by 1 second on Android 14 due to new broadcast receiver restrictions. -- #1175 Bypass the do not disturb permission requirement for volume button triggers. -- #1234 Granting permissions with Shizuku crashes on Android 14. -- #1249 Crash when opening help page from the home page if no browser app for custom tabs was found. -- #1250 Random crashes when picking a screenshot for actions. -- #1227 Deprecate Bluetooth actions on Android 13+ due to new restrictions. -- #1252 Add another Camera key code as supported for screen off triggers. -- #1219 Key Mapper notifications could not be enabled on Android 14. -- #1194 Deprecate closing the status bar on Android 14 due to new restrictions. -- #1190 Add a 3 second delay after the screenshot action before showing the on-screen message +- [#1175](https://github.com/keymapperorg/KeyMapper/issues/1175) Bypass the do not disturb permission requirement for volume button triggers. +- [#1234](https://github.com/keymapperorg/KeyMapper/issues/1234) Granting permissions with Shizuku crashes on Android 14. +- [#1249](https://github.com/keymapperorg/KeyMapper/issues/1249) Crash when opening help page from the home page if no browser app for custom tabs was found. +- [#1250](https://github.com/keymapperorg/KeyMapper/issues/1250) Random crashes when picking a screenshot for actions. +- [#1227](https://github.com/keymapperorg/KeyMapper/issues/1227) Deprecate Bluetooth actions on Android 13+ due to new restrictions. +- [#1252](https://github.com/keymapperorg/KeyMapper/issues/1252) Add another Camera key code as supported for screen off triggers. +- [#1219](https://github.com/keymapperorg/KeyMapper/issues/1219) Key Mapper notifications could not be enabled on Android 14. +- [#1194](https://github.com/keymapperorg/KeyMapper/issues/1194) Deprecate closing the status bar on Android 14 due to new restrictions. +- [#1190](https://github.com/keymapperorg/KeyMapper/issues/1190) Add a 3 second delay after the screenshot action before showing the on-screen message confirming it happened. ## [2.6.0](https://github.com/sds100/KeyMapper/releases/tag/v2.6.0) #### 7 October 2023 -- #550 Action for doing pinches and swipes on the screen with 2 or more fingers. Many thanks to +- [#550](https://github.com/keymapperorg/KeyMapper/issues/550) Action for doing pinches and swipes on the screen with 2 or more fingers. Many thanks to Tino (@pixel-shock) for working on this feature. 😊 ## [2.5.0](https://github.com/sds100/KeyMapper/releases/tag/v2.5.0) @@ -928,63 +928,63 @@ These are all the changes from 2.2.0. - 🎉 A new website with a tutorial! 🎉 [docs.keymapper.club](https://docs.keymapper.club) -- Action to broadcast intent, start activity and start service. #112 -- Action to show the input method picker by using the Key Mapper keyboard. #531 -- Action to toggle the notification drawer and the quick settings drawer. #242 -- Action to call a phone number. #516 +- Action to broadcast intent, start activity and start service. [#112](https://github.com/keymapperorg/KeyMapper/issues/112) +- Action to show the input method picker by using the Key Mapper keyboard. [#531](https://github.com/keymapperorg/KeyMapper/issues/531) +- Action to toggle the notification drawer and the quick settings drawer. [#242](https://github.com/keymapperorg/KeyMapper/issues/242) +- Action to call a phone number. [#516](https://github.com/keymapperorg/KeyMapper/issues/516) - Action to play a sound. - A workaround for the Android 11 bug that sets the language of external keyboards to English-US - when an accessibility service is enabled. #618 Read the guide + when an accessibility service is enabled. [#618](https://github.com/keymapperorg/KeyMapper/issues/618) Read the guide here https://docs.keymapper.club/redirects/android-11-device-id-bug-work-around - Prompt the user to read the quick start guide on the website the first time the app is opened. - #544 -- Links to a relevant online guide in each screen in the app. #539 -- Option in key event action to input the key event through the shell. #559 -- Splash screen #561 -- Data migrations when restoring from backups. #574 -- Enable hold down and disable repeat by default for modifier key actions. #579 -- Ability to change the input method with the accessibility service on Android 11+. #619 -- Make it clearer that selecting a screenshot to set up a tap coordinate action is optional. #632 -- Show a prompt to install the Key Mapper GUI Keyboard when a key event action is created. #645 -- Back up default key map settings in back ups. #659 -- Warnings when the accessibility service is turned on but isn't actually running. #643 -- Show a message at the top of the home screen when mappings are paused. #642 -- A caution message to avoid locking the user when using screen pinning mode. #602 -- A logging page in the app which can be used instead of bug reports. #651 -- A button in the settings to reset sliders to their default. #589 -- A repeat limit action option. #663 + [#544](https://github.com/keymapperorg/KeyMapper/issues/544) +- Links to a relevant online guide in each screen in the app. [#539](https://github.com/keymapperorg/KeyMapper/issues/539) +- Option in key event action to input the key event through the shell. [#559](https://github.com/keymapperorg/KeyMapper/issues/559) +- Splash screen [#561](https://github.com/keymapperorg/KeyMapper/issues/561) +- Data migrations when restoring from backups. [#574](https://github.com/keymapperorg/KeyMapper/issues/574) +- Enable hold down and disable repeat by default for modifier key actions. [#579](https://github.com/keymapperorg/KeyMapper/issues/579) +- Ability to change the input method with the accessibility service on Android 11+. [#619](https://github.com/keymapperorg/KeyMapper/issues/619) +- Make it clearer that selecting a screenshot to set up a tap coordinate action is optional. [#632](https://github.com/keymapperorg/KeyMapper/issues/632) +- Show a prompt to install the Key Mapper GUI Keyboard when a key event action is created. [#645](https://github.com/keymapperorg/KeyMapper/issues/645) +- Back up default key map settings in back ups. [#659](https://github.com/keymapperorg/KeyMapper/issues/659) +- Warnings when the accessibility service is turned on but isn't actually running. [#643](https://github.com/keymapperorg/KeyMapper/issues/643) +- Show a message at the top of the home screen when mappings are paused. [#642](https://github.com/keymapperorg/KeyMapper/issues/642) +- A caution message to avoid locking the user when using screen pinning mode. [#602](https://github.com/keymapperorg/KeyMapper/issues/602) +- A logging page in the app which can be used instead of bug reports. [#651](https://github.com/keymapperorg/KeyMapper/issues/651) +- A button in the settings to reset sliders to their default. [#589](https://github.com/keymapperorg/KeyMapper/issues/589) +- A repeat limit action option. [#663](https://github.com/keymapperorg/KeyMapper/issues/663) - Show a dialog before resetting fingerprint gesture maps. -- A new Key Mapper keyboard that is designed for Android TV. #493 -- An Intent API to pause/resume key maps. #668 -- Allow Key Mapper to be launched from the Android TV launcher. #695 -- Make it much easier to report bugs and turn off aggressive app killing. #728 There is now a button +- A new Key Mapper keyboard that is designed for Android TV. [#493](https://github.com/keymapperorg/KeyMapper/issues/493) +- An Intent API to pause/resume key maps. [#668](https://github.com/keymapperorg/KeyMapper/issues/668) +- Allow Key Mapper to be launched from the Android TV launcher. [#695](https://github.com/keymapperorg/KeyMapper/issues/695) +- Make it much easier to report bugs and turn off aggressive app killing. [#728](https://github.com/keymapperorg/KeyMapper/issues/728) There is now a button in the home screen menu to send a bug report and the user is now prompted to read dontkillmyapp.com when the accessibility service crashes. -- Support for repeat until limit reached action option in fingerprint gesture maps. #710 +- Support for repeat until limit reached action option in fingerprint gesture maps. [#710](https://github.com/keymapperorg/KeyMapper/issues/710) - Polish translations. - Czech translations. ### Changed -- Move action option to show a toast message to the same place as the vibrate option. #565 +- Move action option to show a toast message to the same place as the vibrate option. [#565](https://github.com/keymapperorg/KeyMapper/issues/565) - Replace setting to choose Bluetooth device in settings with setting to choose any input device. - #620 -- Rename 'action count' option to 'how many times'. #611 -- Move option to show the volume ui for an action to when the action is created. #639 -- Tapping the pause/resume key maps notification now opens Key Mapper. #665 -- Make action descriptions more descriptive when repeat is turned on. #666 + [#620](https://github.com/keymapperorg/KeyMapper/issues/620) +- Rename 'action count' option to 'how many times'. [#611](https://github.com/keymapperorg/KeyMapper/issues/611) +- Move option to show the volume ui for an action to when the action is created. [#639](https://github.com/keymapperorg/KeyMapper/issues/639) +- Tapping the pause/resume key maps notification now opens Key Mapper. [#665](https://github.com/keymapperorg/KeyMapper/issues/665) +- Make action descriptions more descriptive when repeat is turned on. [#666](https://github.com/keymapperorg/KeyMapper/issues/666) - Alerts at the top of the home screen have been simplified. ### Removed -- Dex slide in the app intro because it didn't work. #646 -- Buttons to enable all and disable all key maps in the home screen menu. #647 -- Support for Android KitKat 4.4 and older. #627 +- Dex slide in the app intro because it didn't work. [#646](https://github.com/keymapperorg/KeyMapper/issues/646) +- Buttons to enable all and disable all key maps in the home screen menu. [#647](https://github.com/keymapperorg/KeyMapper/issues/647) +- Support for Android KitKat 4.4 and older. [#627](https://github.com/keymapperorg/KeyMapper/issues/627) - Ability to view changelog, license and privacy policy in an in-app dialog. They now open a link in - the browser. #648 + the browser. [#648](https://github.com/keymapperorg/KeyMapper/issues/648) - Alerts at the top of the home screen to enable a Key Mapper keyboard, grant WRITE_SECURE_SETTINGS and grant Do not Disturb mode. @@ -999,7 +999,7 @@ See the 2.3.0 Beta releases below. ### Changes - Never show the "key mapper has crashed" dialog automatically since this causes a lot of confusion. -- Prompt the user to restart the accessibility service rather than report a bug. #736 +- Prompt the user to restart the accessibility service rather than report a bug. [#736](https://github.com/keymapperorg/KeyMapper/issues/736) ### Added @@ -1023,8 +1023,8 @@ See the 2.3.0 Beta releases below. ### Bug Fixes -- Write Secure Settings section in settings is enabled even if permission is revoked. #732 -- Many random crashes. #744, #743, #742, #741, #740, #738, #737 +- Write Secure Settings section in settings is enabled even if permission is revoked. [#732](https://github.com/keymapperorg/KeyMapper/issues/732) +- Many random crashes. [#744](https://github.com/keymapperorg/KeyMapper/issues/744), [#743](https://github.com/keymapperorg/KeyMapper/issues/743), [#742](https://github.com/keymapperorg/KeyMapper/issues/742), [#741](https://github.com/keymapperorg/KeyMapper/issues/741), [#740](https://github.com/keymapperorg/KeyMapper/issues/740), [#738](https://github.com/keymapperorg/KeyMapper/issues/738), [#737](https://github.com/keymapperorg/KeyMapper/issues/737) - Don't crash when restoring back ups without a sounds folder in it. - Don't restore a back up from a newer version of key mapper to prevent the app crashing when reading the restored data. @@ -1035,21 +1035,21 @@ See the 2.3.0 Beta releases below. ### Added -- Make it much easier to report bugs and turn off aggressive app killing. #728 There is now a button +- Make it much easier to report bugs and turn off aggressive app killing. [#728](https://github.com/keymapperorg/KeyMapper/issues/728) There is now a button in the home screen menu to send a bug report and the user is now prompted to read dontkillmyapp.com when the accessibility service crashes. - Action to play a sound ### Bug Fixes -- Close notification drawer after the notification has been pressed. #719 -- Crash if couldn't find input device. #730 -- Crash if couldn't find chosen input method. #731 -- Crash when failing to get package info. #721 -- Crash if couldn't find Bluetooth device. #723 -- Crash when disabling accessibility service. #720 -- Reduce memory usage. #725 -- Ensure log doesn't grow forever. #729 +- Close notification drawer after the notification has been pressed. [#719](https://github.com/keymapperorg/KeyMapper/issues/719) +- Crash if couldn't find input device. [#730](https://github.com/keymapperorg/KeyMapper/issues/730) +- Crash if couldn't find chosen input method. [#731](https://github.com/keymapperorg/KeyMapper/issues/731) +- Crash when failing to get package info. [#721](https://github.com/keymapperorg/KeyMapper/issues/721) +- Crash if couldn't find Bluetooth device. [#723](https://github.com/keymapperorg/KeyMapper/issues/723) +- Crash when disabling accessibility service. [#720](https://github.com/keymapperorg/KeyMapper/issues/720) +- Reduce memory usage. [#725](https://github.com/keymapperorg/KeyMapper/issues/725) +- Ensure log doesn't grow forever. [#729](https://github.com/keymapperorg/KeyMapper/issues/729) ## [2.3.0 Beta 2](https://github.com/sds100/KeyMapper/releases/tag/v2.3.0-beta.02) @@ -1057,14 +1057,14 @@ See the 2.3.0 Beta releases below. ### Added -- Support for repeat until limit reached action option in fingerprint gesture maps. #710 +- Support for repeat until limit reached action option in fingerprint gesture maps. [#710](https://github.com/keymapperorg/KeyMapper/issues/710) ### Bug Fixes -- Crash on start up on some devices. #706 -- Notification advertising fingerprint gesture maps is shown on every update #709 +- Crash on start up on some devices. [#706](https://github.com/keymapperorg/KeyMapper/issues/706) +- Notification advertising fingerprint gesture maps is shown on every update [#709](https://github.com/keymapperorg/KeyMapper/issues/709) - Key map launcher shortcut repeats indefinitely when triggered if repeat until released is chosen. - #707 + [#707](https://github.com/keymapperorg/KeyMapper/issues/707) ## [2.3.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v2.3.0-beta.01) @@ -1077,80 +1077,80 @@ See the 2.3.0 Beta releases below. - 🎉 A new website with a tutorial! 🎉 [docs.keymapper.club](https://docs.keymapper.club) -- Action to broadcast intent, start activity and start service. #112 -- Action to show the input method picker by using the Key Mapper keyboard. #531 -- Action to toggle the notification drawer and the quick settings drawer. #242 -- Action to call a phone number. #516 +- Action to broadcast intent, start activity and start service. [#112](https://github.com/keymapperorg/KeyMapper/issues/112) +- Action to show the input method picker by using the Key Mapper keyboard. [#531](https://github.com/keymapperorg/KeyMapper/issues/531) +- Action to toggle the notification drawer and the quick settings drawer. [#242](https://github.com/keymapperorg/KeyMapper/issues/242) +- Action to call a phone number. [#516](https://github.com/keymapperorg/KeyMapper/issues/516) - A workaround for the Android 11 bug that sets the language of external keyboards to English-US - when an accessibility service is enabled. #618 Read the guide + when an accessibility service is enabled. [#618](https://github.com/keymapperorg/KeyMapper/issues/618) Read the guide here https://docs.keymapper.club/redirects/android-11-device-id-bug-work-around - Prompt the user to read the quick start guide on the website the first time the app is opened. - #544 -- Links to a relevant online guide in each screen in the app. #539 -- Option in key event action to input the key event through the shell. #559 -- Splash screen #561 -- Data migrations when restoring from backups. #574 -- Enable hold down and disable repeat by default for modifier key actions. #579 -- Ability to change the input method with the accessibility service on Android 11+. #619 -- Make it clearer that selecting a screenshot to set up a tap coordinate action is optional. #632 -- Show a prompt to install the Key Mapper GUI Keyboard when a key event action is created. #645 -- Back up default key map settings in back ups. #659 -- Warnings when the accessibility service is turned on but isn't actually running. #643 -- Show a message at the top of the home screen when mappings are paused. #642 -- A caution message to avoid locking the user when using screen pinning mode. #602 -- A logging page in the app which can be used instead of bug reports. #651 -- A button in the settings to reset sliders to their default. #589 -- A repeat limit action option. #663 + [#544](https://github.com/keymapperorg/KeyMapper/issues/544) +- Links to a relevant online guide in each screen in the app. [#539](https://github.com/keymapperorg/KeyMapper/issues/539) +- Option in key event action to input the key event through the shell. [#559](https://github.com/keymapperorg/KeyMapper/issues/559) +- Splash screen [#561](https://github.com/keymapperorg/KeyMapper/issues/561) +- Data migrations when restoring from backups. [#574](https://github.com/keymapperorg/KeyMapper/issues/574) +- Enable hold down and disable repeat by default for modifier key actions. [#579](https://github.com/keymapperorg/KeyMapper/issues/579) +- Ability to change the input method with the accessibility service on Android 11+. [#619](https://github.com/keymapperorg/KeyMapper/issues/619) +- Make it clearer that selecting a screenshot to set up a tap coordinate action is optional. [#632](https://github.com/keymapperorg/KeyMapper/issues/632) +- Show a prompt to install the Key Mapper GUI Keyboard when a key event action is created. [#645](https://github.com/keymapperorg/KeyMapper/issues/645) +- Back up default key map settings in back ups. [#659](https://github.com/keymapperorg/KeyMapper/issues/659) +- Warnings when the accessibility service is turned on but isn't actually running. [#643](https://github.com/keymapperorg/KeyMapper/issues/643) +- Show a message at the top of the home screen when mappings are paused. [#642](https://github.com/keymapperorg/KeyMapper/issues/642) +- A caution message to avoid locking the user when using screen pinning mode. [#602](https://github.com/keymapperorg/KeyMapper/issues/602) +- A logging page in the app which can be used instead of bug reports. [#651](https://github.com/keymapperorg/KeyMapper/issues/651) +- A button in the settings to reset sliders to their default. [#589](https://github.com/keymapperorg/KeyMapper/issues/589) +- A repeat limit action option. [#663](https://github.com/keymapperorg/KeyMapper/issues/663) - Show a dialog before resetting fingerprint gesture maps. -- A new Key Mapper keyboard that is designed for Android TV. #493 -- An Intent API to pause/resume key maps. #668 -- Allow Key Mapper to be launched from the Android TV launcher. #695 +- A new Key Mapper keyboard that is designed for Android TV. [#493](https://github.com/keymapperorg/KeyMapper/issues/493) +- An Intent API to pause/resume key maps. [#668](https://github.com/keymapperorg/KeyMapper/issues/668) +- Allow Key Mapper to be launched from the Android TV launcher. [#695](https://github.com/keymapperorg/KeyMapper/issues/695) ### Changed -- Move action option to show a toast message to the same place as the vibrate option. #565 +- Move action option to show a toast message to the same place as the vibrate option. [#565](https://github.com/keymapperorg/KeyMapper/issues/565) - Replace setting to choose Bluetooth device in settings with setting to choose any input device. - #620 -- Rename 'action count' option to 'how many times'. #611 -- Move option to show the volume ui for an action to when the action is created. #639 -- Tapping the pause/resume key maps notification now opens Key Mapper. #665 -- Make action descriptions more descriptive when repeat is turned on. #666 + [#620](https://github.com/keymapperorg/KeyMapper/issues/620) +- Rename 'action count' option to 'how many times'. [#611](https://github.com/keymapperorg/KeyMapper/issues/611) +- Move option to show the volume ui for an action to when the action is created. [#639](https://github.com/keymapperorg/KeyMapper/issues/639) +- Tapping the pause/resume key maps notification now opens Key Mapper. [#665](https://github.com/keymapperorg/KeyMapper/issues/665) +- Make action descriptions more descriptive when repeat is turned on. [#666](https://github.com/keymapperorg/KeyMapper/issues/666) - Alerts at the top of the home screen have been simplified. ### Removed -- Dex slide in the app intro because it didn't work. #646 -- Buttons to enable all and disable all key maps in the home screen menu. #647 -- Support for Android KitKat 4.4 and older. #627 +- Dex slide in the app intro because it didn't work. [#646](https://github.com/keymapperorg/KeyMapper/issues/646) +- Buttons to enable all and disable all key maps in the home screen menu. [#647](https://github.com/keymapperorg/KeyMapper/issues/647) +- Support for Android KitKat 4.4 and older. [#627](https://github.com/keymapperorg/KeyMapper/issues/627) - Ability to view changelog, license and privacy policy in an in-app dialog. They now open a link in - the browser. #648 + the browser. [#648](https://github.com/keymapperorg/KeyMapper/issues/648) - Alerts at the top of the home screen to enable a Key Mapper keyboard, grant WRITE_SECURE_SETTINGS and grant Do not Disturb mode. ### Bug fixes -- Fix jank #549 -- Fix text consistency #543 +- Fix jank [#549](https://github.com/keymapperorg/KeyMapper/issues/549) +- Fix text consistency [#543](https://github.com/keymapperorg/KeyMapper/issues/543) - A parallel trigger which contains another parallel trigger after the first key should cancel the - other. #571 -- Actions go off screen for key maps on the home screen. #613 -- Remove uses of Android framework strings for dialog buttons. #650 -- Trigger key click type sometimes resets to short press. #615 + other. [#571](https://github.com/keymapperorg/KeyMapper/issues/571) +- Actions go off screen for key maps on the home screen. [#613](https://github.com/keymapperorg/KeyMapper/issues/613) +- Remove uses of Android framework strings for dialog buttons. [#650](https://github.com/keymapperorg/KeyMapper/issues/650) +- Trigger key click type sometimes resets to short press. [#615](https://github.com/keymapperorg/KeyMapper/issues/615) - Wrong device id is used when performing key event actions and there are multiple devices with the - same descriptor. #637 -- Trigger key isn't imitated after a failed double press. #606 -- Actions don't start repeating on a failed long press or failed double press. #626 -- Crash when modifying a huge number of key maps. #641 -- Home menu is chopped off on screens with small height. #582 -- Crash when double pressing button to open action or trigger key options. #600 -- Some action options disappear when adding a new trigger key. #594 + same descriptor. [#637](https://github.com/keymapperorg/KeyMapper/issues/637) +- Trigger key isn't imitated after a failed double press. [#606](https://github.com/keymapperorg/KeyMapper/issues/606) +- Actions don't start repeating on a failed long press or failed double press. [#626](https://github.com/keymapperorg/KeyMapper/issues/626) +- Crash when modifying a huge number of key maps. [#641](https://github.com/keymapperorg/KeyMapper/issues/641) +- Home menu is chopped off on screens with small height. [#582](https://github.com/keymapperorg/KeyMapper/issues/582) +- Crash when double pressing button to open action or trigger key options. [#600](https://github.com/keymapperorg/KeyMapper/issues/600) +- Some action options disappear when adding a new trigger key. [#594](https://github.com/keymapperorg/KeyMapper/issues/594) - An action can continue to repeat even when the trigger is released if delay until next action is - not 0. #662 -- A lot of input latency when using a lot of constraints. #599 + not 0. [#662](https://github.com/keymapperorg/KeyMapper/issues/662) +- A lot of input latency when using a lot of constraints. [#599](https://github.com/keymapperorg/KeyMapper/issues/599) - Trigger button isn't imitated when a short press trigger with multiple keys fails to be triggered. - #664 -- Overlapping triggers. #653 + [#664](https://github.com/keymapperorg/KeyMapper/issues/664) +- Overlapping triggers. [#653](https://github.com/keymapperorg/KeyMapper/issues/653) ## [2.2.0](https://github.com/sds100/KeyMapper/releases/tag/v2.2.0) @@ -1160,26 +1160,26 @@ This sums up all the changes for 2.2 ### Added -- Remap fingerprint gestures! #378 Android 8.0+ and only on devices which support them. Even devices +- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android 8.0+ and only on devices which support them. Even devices with the setting to swipe down for notifications might not support this! The dev can't do anything about this. -- Widget/shortcut to launch actions. #459 +- Widget/shortcut to launch actions. [#459](https://github.com/keymapperorg/KeyMapper/issues/459) - Setting to show the first 5 digits of input devices so devices with the same name can be - differentiated in Key Mapper lists. #470 + differentiated in Key Mapper lists. [#470](https://github.com/keymapperorg/KeyMapper/issues/470) - Show a warning at the top of the homescreen if the user hasn't disabled battery optimisation for - Key Mapper. #496 -- Action option to hold down until the trigger is pressed again. #479 -- Action option to change the delay before the next action in the list. #476 -- Orientation constraint. #505 -- Key Event action option to pretend that the Key Event came from a particular device. #509 -- Use duplicates of the same key in a sequence trigger. #513 -- Show the fingerprint gesture intro slide when updating to 2.2 #545 + Key Mapper. [#496](https://github.com/keymapperorg/KeyMapper/issues/496) +- Action option to hold down until the trigger is pressed again. [#479](https://github.com/keymapperorg/KeyMapper/issues/479) +- Action option to change the delay before the next action in the list. [#476](https://github.com/keymapperorg/KeyMapper/issues/476) +- Orientation constraint. [#505](https://github.com/keymapperorg/KeyMapper/issues/505) +- Key Event action option to pretend that the Key Event came from a particular device. [#509](https://github.com/keymapperorg/KeyMapper/issues/509) +- Use duplicates of the same key in a sequence trigger. [#513](https://github.com/keymapperorg/KeyMapper/issues/513) +- Show the fingerprint gesture intro slide when updating to 2.2 [#545](https://github.com/keymapperorg/KeyMapper/issues/545) - Show a silent notification, which advertises the remapping fingerprint gesture feature, when the - user updates to 2.2 #546 -- Trigger key maps from an Intent #490 + user updates to 2.2 [#546](https://github.com/keymapperorg/KeyMapper/issues/546) +- Trigger key maps from an Intent [#490](https://github.com/keymapperorg/KeyMapper/issues/490) - Prompt the user to go to https://dontkillmyapp.com when they first setup the app. -- Add Fdroid link to the Key Mapper GUI Keyboard ad. #524 +- Add Fdroid link to the Key Mapper GUI Keyboard ad. [#524](https://github.com/keymapperorg/KeyMapper/issues/524) ### BREAKING CHANGES @@ -1188,7 +1188,7 @@ This sums up all the changes for 2.2 ### Changes -- No max limit for sliders (except in settings). #458 +- No max limit for sliders (except in settings). [#458](https://github.com/keymapperorg/KeyMapper/issues/458) - The app intro slides will show feedback if the steps have been done correctly. ### Removed @@ -1197,15 +1197,15 @@ This sums up all the changes for 2.2 ### Bug Fixes -- Save and restore state for all view models. #519 -- Use View Binding in fragments properly. This should stop random crashes for some users. #518 -- Hold Down action option doesn't work for long press triggers. #504 +- Save and restore state for all view models. [#519](https://github.com/keymapperorg/KeyMapper/issues/519) +- Use View Binding in fragments properly. This should stop random crashes for some users. [#518](https://github.com/keymapperorg/KeyMapper/issues/518) +- Hold Down action option doesn't work for long press triggers. [#504](https://github.com/keymapperorg/KeyMapper/issues/504) - A trigger for a specific device can still be detected if the same buttons on another device are - pressed. #523 -- Fix layout of the trigger fragment on some screen sizes so that some things aren't cut off. #522 -- Remapping modifier keys to the same key didn't work as expected. #563 -- Parallel triggers which contained another parallel trigger didn't cancel the other. #571 -- Don't allow screen on/off constraints for fingerprint gestures #570 + pressed. [#523](https://github.com/keymapperorg/KeyMapper/issues/523) +- Fix layout of the trigger fragment on some screen sizes so that some things aren't cut off. [#522](https://github.com/keymapperorg/KeyMapper/issues/522) +- Remapping modifier keys to the same key didn't work as expected. [#563](https://github.com/keymapperorg/KeyMapper/issues/563) +- Parallel triggers which contained another parallel trigger didn't cancel the other. [#571](https://github.com/keymapperorg/KeyMapper/issues/571) +- Don't allow screen on/off constraints for fingerprint gestures [#570](https://github.com/keymapperorg/KeyMapper/issues/570) - Rename Key Mapper CI Keyboard to Key Mapper CI Basic Input Method. - Notifications had no icon on Android Lollipop. - remove coloured navigation bar on Android Lollipop. @@ -1213,15 +1213,15 @@ This sums up all the changes for 2.2 - Detecting whether remapping fingerprint gestures are supported didn't work. - The flashlight action would sometimes crash the app. - The error message for an app being disabled was the wrong one. -- Actions to open Android TV apps didn't work #503 -- The app list didn't show Android TV-only apps. #487 +- Actions to open Android TV apps didn't work [#503](https://github.com/keymapperorg/KeyMapper/issues/503) +- The app list didn't show Android TV-only apps. [#487](https://github.com/keymapperorg/KeyMapper/issues/487) - Settings for repeat rate and delay until repeat didn't match their names when configuring an action. -- Text would move up/down when sliding between slides in the app intro. #540 -- Icon for "specific app playing media" constraint had the wrong tint. #535 +- Text would move up/down when sliding between slides in the app intro. [#540](https://github.com/keymapperorg/KeyMapper/issues/540) +- Icon for "specific app playing media" constraint had the wrong tint. [#535](https://github.com/keymapperorg/KeyMapper/issues/535) - Limit Media actions to Android 4.4 KitKat+ because they don't work on older versions. - Up Key Event was sent from all keymaps with the "hold down" action option regardless of whether - the trigger was released. #533 + the trigger was released. [#533](https://github.com/keymapperorg/KeyMapper/issues/533) - Testing actions didn't work. - Scroll position was lost when reloading the key map list. - Try to fix random crashes when navigating. @@ -1233,15 +1233,15 @@ This sums up all the changes for 2.2 ### Added -- Remap fingerprint gestures! #378 Android 8.0+ and only on devices which support them. Even devices +- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android 8.0+ and only on devices which support them. Even devices with the setting to swipe down for notifications might not support this! The dev can't do anything about this. -- Show the fingerprint gesture intro slide when updating to 2.2 #545 +- Show the fingerprint gesture intro slide when updating to 2.2 [#545](https://github.com/keymapperorg/KeyMapper/issues/545) - Show a silent notification, which advertises the remapping fingerprint gesture feature, when the - user updates to 2.2 #546 -- Trigger key maps from an Intent #490 + user updates to 2.2 [#546](https://github.com/keymapperorg/KeyMapper/issues/546) +- Trigger key maps from an Intent [#490](https://github.com/keymapperorg/KeyMapper/issues/490) - Prompt the user to go to https://dontkillmyapp.com when they first setup the app. -- Add Fdroid link to the Key Mapper GUI Keyboard ad. #524 +- Add Fdroid link to the Key Mapper GUI Keyboard ad. [#524](https://github.com/keymapperorg/KeyMapper/issues/524) ### BREAKING CHANGES @@ -1258,9 +1258,9 @@ This sums up all the changes for 2.2 ### Bug Fixes -- Remapping modifier keys to the same key didn't work as expected. #563 -- Parallel triggers which contained another parallel trigger didn't cancel the other. #571 -- Don't allow screen on/off constraints for fingerprint gestures #570 +- Remapping modifier keys to the same key didn't work as expected. [#563](https://github.com/keymapperorg/KeyMapper/issues/563) +- Parallel triggers which contained another parallel trigger didn't cancel the other. [#571](https://github.com/keymapperorg/KeyMapper/issues/571) +- Don't allow screen on/off constraints for fingerprint gestures [#570](https://github.com/keymapperorg/KeyMapper/issues/570) - Rename Key Mapper CI Keyboard to Key Mapper CI Basic Input Method. - Notifications had no icon on Android Lollipop. - remove coloured navigation bar on Android Lollipop. @@ -1268,15 +1268,15 @@ This sums up all the changes for 2.2 - Detecting whether remapping fingerprint gestures are supported didn't work. - The flashlight action would sometimes crash the app. - The error message for an app being disabled was the wrong one. -- Actions to open Android TV apps didn't work #503 -- The app list didn't show Android TV-only apps. #487 +- Actions to open Android TV apps didn't work [#503](https://github.com/keymapperorg/KeyMapper/issues/503) +- The app list didn't show Android TV-only apps. [#487](https://github.com/keymapperorg/KeyMapper/issues/487) - Settings for repeat rate and delay until repeat didn't match their names when configuring an action. -- Text would move up/down when sliding between slides in the app intro. #540 -- Icon for "specific app playing media" constraint had the wrong tint. #535 +- Text would move up/down when sliding between slides in the app intro. [#540](https://github.com/keymapperorg/KeyMapper/issues/540) +- Icon for "specific app playing media" constraint had the wrong tint. [#535](https://github.com/keymapperorg/KeyMapper/issues/535) - Limit Media actions to Android 4.4 KitKat+ because they don't work on older versions. - Up Key Event was sent from all keymaps with the "hold down" action option regardless of whether - the trigger was released. #533 + the trigger was released. [#533](https://github.com/keymapperorg/KeyMapper/issues/533) - Testing actions didn't work. - Scroll position was lost when reloading the key map list. - Try to fix random crashes when navigating. @@ -1288,35 +1288,35 @@ This sums up all the changes for 2.2 ### Added -- Remap fingerprint gestures! #378 Android 8.0+ and only on devices which support them. Even devices +- Remap fingerprint gestures! [#378](https://github.com/keymapperorg/KeyMapper/issues/378) Android 8.0+ and only on devices which support them. Even devices with the setting to swipe down for notifications might not support this! The dev can't do anything about this. -- Widget/shortcut to launch actions. #459 +- Widget/shortcut to launch actions. [#459](https://github.com/keymapperorg/KeyMapper/issues/459) - Setting to show the first 5 digits of input devices so devices with the same name can be - differentiated in Key Mapper lists. #470 + differentiated in Key Mapper lists. [#470](https://github.com/keymapperorg/KeyMapper/issues/470) - Show a warning at the top of the homescreen if the user hasn't disabled battery optimisation for - Key Mapper. #496 -- Action option to hold down until the trigger is pressed again. #479 -- Action option to change the delay before the next action in the list. #476 -- Orientation constraint. #505 -- Constraint for when a specific app is playing media. #508 -- Key Event action option to pretend that the Key Event came from a particular device. #509 -- Use duplicates of the same key in a sequence trigger. #513 -- Hold down repeatedly if repeat and hold down are enabled. #500 + Key Mapper. [#496](https://github.com/keymapperorg/KeyMapper/issues/496) +- Action option to hold down until the trigger is pressed again. [#479](https://github.com/keymapperorg/KeyMapper/issues/479) +- Action option to change the delay before the next action in the list. [#476](https://github.com/keymapperorg/KeyMapper/issues/476) +- Orientation constraint. [#505](https://github.com/keymapperorg/KeyMapper/issues/505) +- Constraint for when a specific app is playing media. [#508](https://github.com/keymapperorg/KeyMapper/issues/508) +- Key Event action option to pretend that the Key Event came from a particular device. [#509](https://github.com/keymapperorg/KeyMapper/issues/509) +- Use duplicates of the same key in a sequence trigger. [#513](https://github.com/keymapperorg/KeyMapper/issues/513) +- Hold down repeatedly if repeat and hold down are enabled. [#500](https://github.com/keymapperorg/KeyMapper/issues/500) ### Changes -- No max limit for sliders (except in settings). #458 +- No max limit for sliders (except in settings). [#458](https://github.com/keymapperorg/KeyMapper/issues/458) ### Bug Fixes -- Save and restore state for all view models. #519 -- Use View Binding in fragments properly. This should stop random crashes for some users. #518 -- Hold Down action option doesn't work for long press triggers. #504 +- Save and restore state for all view models. [#519](https://github.com/keymapperorg/KeyMapper/issues/519) +- Use View Binding in fragments properly. This should stop random crashes for some users. [#518](https://github.com/keymapperorg/KeyMapper/issues/518) +- Hold Down action option doesn't work for long press triggers. [#504](https://github.com/keymapperorg/KeyMapper/issues/504) - A trigger for a specific device can still be detected if the same buttons on another device are - pressed. #523 -- Fix layout of the trigger fragment on some screen sizes so that some things aren't cut off. #522 + pressed. [#523](https://github.com/keymapperorg/KeyMapper/issues/523) +- Fix layout of the trigger fragment on some screen sizes so that some things aren't cut off. [#522](https://github.com/keymapperorg/KeyMapper/issues/522) ## [2.1.0](https://github.com/sds100/KeyMapper/releases/tag/v2.1.0) @@ -1729,7 +1729,7 @@ This is the first release to be released on F-Droid. ### Bug Fix -- KEYCODE_BACK appeared twice in the keycode action list. #247 +- KEYCODE_BACK appeared twice in the keycode action list. [#247](https://github.com/keymapperorg/KeyMapper/issues/247) ## [1.1.4](https://github.com/sds100/KeyMapper/releases/tag/v1.1.4) From d5f64a91efdefcab8ce647ed25928d1f90f2e02f Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 13:18:29 +0100 Subject: [PATCH 22/48] style: reformat --- app/src/main/java/io/github/sds100/keymapper/MainActivity.kt | 1 - .../io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt | 1 - .../sds100/keymapper/base/settings/ConfigSettingsUseCase.kt | 1 - .../github/sds100/keymapper/base/settings/SettingsViewModel.kt | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt b/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt index 278186a9e1..d48551a3dc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt @@ -8,7 +8,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.BaseMainActivity import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.databinding.ActivityMainBinding -import io.github.sds100.keymapper.base.settings.AppLocaleAdapter import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.showDialogs import javax.inject.Inject diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt index 66d2207e9b..ad7fb1ee08 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base import dagger.Binds import dagger.Module import dagger.hilt.InstallIn -import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.components.SingletonComponent import io.github.sds100.keymapper.base.actions.GetActionErrorUseCase import io.github.sds100.keymapper.base.actions.GetActionErrorUseCaseImpl diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index d4ece5baf1..ddf8d4ce88 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -4,7 +4,6 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.datastore.preferences.core.Preferences import dagger.hilt.android.scopes.ViewModelScoped -import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.sound.SoundFileInfo import io.github.sds100.keymapper.base.actions.sound.SoundsManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 1fd60bb3bf..74896e05a7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -22,6 +22,7 @@ import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -29,7 +30,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( From f31b2c38d17719fd0b2503f154450d905fcb653f Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 13:22:09 +0100 Subject: [PATCH 23/48] style: reformat --- .../core/src/android/keylayout/generic_key_layout.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs index b454076831..ac7f35eb89 100644 --- a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs +++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs @@ -286,4 +286,3 @@ axis 0x0a LTRIGGER axis 0x10 HAT_X axis 0x11 HAT_Y "#; - From bd08ee20cc6a8b8123f0746663d9457264e841a6 Mon Sep 17 00:00:00 2001 From: Jack Ambler <54366245+jambl3r@users.noreply.github.com> Date: Sat, 17 Jan 2026 12:26:01 +0000 Subject: [PATCH 24/48] fix: corrected discord link in app listing --- fastlane/metadata/android/en-US/full_description.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 476e968105..b4209b2c58 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -46,10 +46,10 @@ It will NOT collect any user data or connect to the internet to send any data an Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. Come say hi in our Discord community! -discord.keymapper.app +keymapper.app/discord See the code for yourself! (Open source) github.com/keymapperorg/KeyMapper Read the documentation: -keymapper.app \ No newline at end of file +keymapper.app From ac6cf6d867a4978562ac1634ae1d7f51210e4877 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 13:43:17 +0100 Subject: [PATCH 25/48] make log less verbose --- .../sds100/keymapper/base/input/EvdevDevicesDelegate.kt | 4 ++-- .../core/src/android/keylayout/key_layout_map_manager.rs | 4 ++-- .../rust/evdev_manager/core/src/evdev_grab_controller.rs | 4 ++-- .../keymapper/system/bluetooth/AndroidBluetoothAdapter.kt | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt index 9dc7ea17d5..93b77346eb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt @@ -92,7 +92,7 @@ class EvdevDevicesDelegate @Inject constructor( } fun onGrabbedDevicesChanged(devices: List) { - Timber.i("Grabbed devices changed: [${devices.joinToString { it.name }}]") + Timber.d("Grabbed devices changed: [${devices.joinToString { it.name }}]") grabbedDevicesById.value = devices.associate { handle -> @@ -102,7 +102,7 @@ class EvdevDevicesDelegate @Inject constructor( } fun onEvdevDevicesChanged(devices: List) { - Timber.i("Evdev devices changed: [${devices.joinToString { it.name }}]") + Timber.d("Evdev devices changed: [${devices.joinToString { it.name }}]") allDevices.value = devices } diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_map_manager.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_map_manager.rs index 632c72d970..bca364df58 100644 --- a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_map_manager.rs +++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_map_manager.rs @@ -127,7 +127,7 @@ impl KeyLayoutMapManager { } let key_layout_map_paths = self.find_key_layout_files(device_info); - info!( + debug!( "Found key layout map files for device {}: {:?}", device_info.name, key_layout_map_paths ); @@ -148,7 +148,7 @@ impl KeyLayoutMapManager { // No key layout map files were found or parsed successfully. // Fall back to the hardcoded generic key layout map. - info!( + debug!( "No key layout files found for device {}, using hardcoded Generic fallback", device_info.name ); diff --git a/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs b/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs index 78c4d4a8cb..b43893ac3e 100644 --- a/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs +++ b/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs @@ -42,7 +42,7 @@ impl EvdevGrabController { } pub fn set_grab_targets(&self, targets: Vec) -> Vec { - info!("Setting grab targets: {:?}", targets); + debug!("Setting grab targets: {:?}", targets); let mut grab_targets = self.grab_targets.lock().unwrap(); @@ -96,7 +96,7 @@ impl EvdevGrabController { .map(|(key, device)| GrabbedDeviceHandle::new(key, device.device_info.clone())) .collect(); - info!("Grabbed devices: {:?}", grabbed_device_handles); + debug!("Grabbed devices: {:?}", grabbed_device_handles); self.callback .on_grabbed_devices_changed(grabbed_device_handles.clone()); diff --git a/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt index 547c6a5d1a..f5685c0151 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt @@ -77,7 +77,7 @@ class AndroidBluetoothAdapter @Inject constructor( val address = device.address ?: return@launch val name = device.name ?: return@launch - Timber.i("On Bluetooth device connected: $name") + Timber.d("On Bluetooth device connected: $name") onDeviceConnect.emit( BluetoothDeviceInfo( @@ -97,7 +97,7 @@ class AndroidBluetoothAdapter @Inject constructor( val address = device.address ?: return@launch val name = device.name ?: return@launch - Timber.i("On Bluetooth device disconnected: $name") + Timber.d("On Bluetooth device disconnected: $name") onDeviceDisconnect.emit( BluetoothDeviceInfo( @@ -118,7 +118,7 @@ class AndroidBluetoothAdapter @Inject constructor( val name = device.name ?: return@launch val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1) - Timber.i("On Bluetooth device bond state changed to $bondState: $name") + Timber.d("On Bluetooth device bond state changed to $bondState: $name") onDevicePairedChange.emit( BluetoothDeviceInfo( From e372c762bfd583d715b56b60f318e122ddc81972 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 19:59:07 +0100 Subject: [PATCH 26/48] fix: NetworkAdapter now updates isWifiConnected correctly --- .../system/network/AndroidNetworkAdapter.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index e7c9085d1e..05393a900a 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -9,6 +9,7 @@ import android.content.IntentFilter import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities +import android.net.NetworkInfo import android.net.NetworkRequest import android.net.wifi.WifiInfo import android.net.wifi.WifiManager @@ -18,6 +19,7 @@ import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat +import androidx.core.content.IntentCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.Constants @@ -56,6 +58,7 @@ class AndroidNetworkAdapter @Inject constructor( private val httpClient: OkHttpClient by lazy { OkHttpClient() } private val broadcastReceiver = object : BroadcastReceiver() { + @Suppress("DEPRECATION") override fun onReceive(context: Context?, intent: Intent?) { intent?.action ?: return @@ -64,10 +67,22 @@ class AndroidNetworkAdapter @Inject constructor( val state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, -1) isWifiEnabled.update { state == WifiManager.WIFI_STATE_ENABLED } + + if (state != WifiManager.WIFI_STATE_ENABLED) { + isWifiConnected.value = false + connectedWifiSSIDFlow.value = null + } } WifiManager.NETWORK_STATE_CHANGED_ACTION -> { + val networkInfo = IntentCompat.getParcelableExtra( + intent, + WifiManager.EXTRA_NETWORK_INFO, + NetworkInfo::class.java, + ) ?: return + connectedWifiSSIDFlow.update { getWifiSSID() } + isWifiConnected.update { networkInfo.isConnected } } } } From 4c615303775702eb1bda7fa1a8627deba41a34a3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 20:18:36 +0100 Subject: [PATCH 27/48] chore: upgrade foss gradle wrapper version --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3270e14c12..eed656c6ea 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Jun 20 18:26:00 BST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 4a3b1174fc1371fea93949bc547f46b57fa12242 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 20:19:25 +0100 Subject: [PATCH 28/48] fix: do not momentarily show pair adb steps after connecting to a wifi network --- .../expertmode/SystemBridgeSetupUseCase.kt | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt index 37fbf7319f..a35ca5f30d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt @@ -23,13 +23,13 @@ import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import javax.inject.Inject -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) @@ -58,18 +58,29 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( override val isWarningUnderstood: Flow = preferences.get(Keys.isExpertModeWarningUnderstood).map { it ?: false } - private val isAdbAutoStartAllowed: Flow = + private val adbAutoStartEligibility: Flow = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { combine( permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), networkAdapter.isWifiConnected, ) { isWriteSecureSettingsGranted, isWifiConnected -> - isWriteSecureSettingsGranted && - isWifiConnected && - systemBridgeSetupController.isAdbPaired() - }.flowOn(Dispatchers.IO) + isWriteSecureSettingsGranted && isWifiConnected + }.flatMapLatest { canCheck -> + if (canCheck) { + flow { + emit(AdbAutoStartEligibility.CHECKING) + if (systemBridgeSetupController.isAdbPaired()) { + emit(AdbAutoStartEligibility.ELIGIBLE) + } else { + emit(AdbAutoStartEligibility.NOT_ELIGIBLE) + } + } + } else { + flowOf(AdbAutoStartEligibility.NOT_ELIGIBLE) + } + } } else { - flowOf(false) + flowOf(AdbAutoStartEligibility.NOT_ELIGIBLE) } override fun onUnderstoodWarning() { @@ -108,11 +119,14 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( if (isConnected) { flowOf(SystemBridgeSetupStep.STARTED) } else { - isAdbAutoStartAllowed.flatMapLatest { isAdbAutoStartAllowed -> - if (isAdbAutoStartAllowed) { - flowOf(SystemBridgeSetupStep.START_SERVICE) - } else { - combine( + adbAutoStartEligibility.flatMapLatest { adbAutoStartEligibility -> + when (adbAutoStartEligibility) { + AdbAutoStartEligibility.ELIGIBLE -> + flowOf(SystemBridgeSetupStep.START_SERVICE) + + AdbAutoStartEligibility.CHECKING -> emptyFlow() + + AdbAutoStartEligibility.NOT_ELIGIBLE -> combine( accessibilityServiceAdapter.state, isNotificationPermissionGranted, systemBridgeSetupController.isDeveloperOptionsEnabled, @@ -347,3 +361,9 @@ interface SystemBridgeSetupUseCase { suspend fun getShellStartCommand(): KMResult } + +enum class AdbAutoStartEligibility { + ELIGIBLE, + CHECKING, + NOT_ELIGIBLE, +} From 7a8b2f6c9cd4875a089a51514de876c0e9760240 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 20:22:28 +0100 Subject: [PATCH 29/48] fix: move switch to end of SwitchText and make text fill space --- .../base/utils/ui/compose/SwitchText.kt | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchText.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchText.kt index 8a362b7c4f..7be076f99a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchText.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchText.kt @@ -2,7 +2,9 @@ package io.github.sds100.keymapper.base.utils.ui.compose import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -25,24 +27,16 @@ fun SwitchText( Surface( modifier = modifier, shape = MaterialTheme.shapes.medium, - color = Color.Companion.Transparent, + color = Color.Transparent, ) { Row( - modifier = Modifier.Companion + modifier = Modifier .clickable(enabled = isEnabled) { onCheckedChange(!isChecked) } .padding(8.dp), - verticalAlignment = Alignment.Companion.CenterVertically, + verticalAlignment = Alignment.CenterVertically, ) { - Switch( - enabled = isEnabled, - checked = isChecked, - // This is null so tapping on the checkbox highlights the whole row. - onCheckedChange = null, - ) - Text( - modifier = Modifier.Companion.padding(horizontal = 12.dp), - + modifier = Modifier.weight(1f), text = text, style = if (isEnabled) { MaterialTheme.typography.bodyLarge @@ -54,7 +48,16 @@ fun SwitchText( ) }, maxLines = 2, - overflow = TextOverflow.Companion.Ellipsis, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(Modifier.width(16.dp)) + + Switch( + enabled = isEnabled, + checked = isChecked, + // This is null so tapping on the checkbox highlights the whole row. + onCheckedChange = null, ) } } From afe5d702d0ddcddff61591774a3fb0620aca8625 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 20:29:47 +0100 Subject: [PATCH 30/48] make floating button config screen more compact --- .../sds100/keymapper/base/utils/ui/compose/SwitchText.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchText.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchText.kt index 7be076f99a..59351b4191 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchText.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchText.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -15,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp @Composable fun SwitchText( @@ -32,11 +34,15 @@ fun SwitchText( Row( modifier = Modifier .clickable(enabled = isEnabled) { onCheckedChange(!isChecked) } - .padding(8.dp), + .padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( modifier = Modifier.weight(1f), + autoSize = TextAutoSize.StepBased( + minFontSize = 10.sp, + maxFontSize = MaterialTheme.typography.bodyLarge.fontSize, + ), text = text, style = if (isEnabled) { MaterialTheme.typography.bodyLarge From 4eafe1929f4716e81be7394105d0e24db156c179 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 20:39:40 +0100 Subject: [PATCH 31/48] #1961 fix: disabling setup assistant shows a notification asking for pairing code immediately --- CHANGELOG.md | 1 + .../SystemBridgeSetupAssistantController.kt | 35 ++++++++++++++----- base/src/main/res/values/strings.xml | 3 ++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa8fc01f3..417c9f43b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - [#1972](https://github.com/keymapperorg/KeyMapper/issues/1972) Expert Mode works on Android 10. - [#1976](https://github.com/keymapperorg/KeyMapper/issues/1976) Panic in Rust system bridge code on some devices. - [#1971](https://github.com/keymapperorg/KeyMapper/issues/1971) Media actions work again in some apps, like YouTube. +- [#1961](https://github.com/keymapperorg/KeyMapper/issues/1961) Disabling setup assistant shows a notification asking for pairing code immediately. - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt index 73f14b3120..dca027700e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt @@ -315,18 +315,37 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } SystemBridgeSetupStep.ADB_PAIRING -> { - showNotification( - getString(R.string.expert_mode_setup_notification_pairing_title), - getString(R.string.expert_mode_setup_notification_pairing_text), - ) + if (isInteractive.value) { + showNotification( + getString(R.string.expert_mode_setup_notification_pairing_title), + getString(R.string.expert_mode_setup_notification_pairing_text), + ) + interactionStep = InteractionStep.PAIR_DEVICE + startInteractionTimeoutJob() + } else { + // Show a notification asking for pairing code straight away if interaction + // is disabled. + showNotification( + title = getString( + R.string.expert_mode_setup_notification_pairing_code_not_interactive_title, + ), + text = getString( + R.string.expert_mode_setup_notification_pairing_code_not_interactive_text, + ), + actions = listOf( + KMNotificationAction.RemoteInput.PairingCode to + getString( + R.string.expert_mode_setup_notification_action_input_pairing_code, + ), + ), + ) - interactionStep = InteractionStep.PAIR_DEVICE + interactionStep = null + } } - else -> return // Do not start interaction timeout job + else -> return } - - startInteractionTimeoutJob() } private fun startInteractionTimeoutJob() { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 90b5ba20ad..18e589fc2b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1757,6 +1757,9 @@ Unable to find pairing port and code Tap on the button to pair with pairing code and type the code in here + Type in the pairing code + Tap \"Pair device with pairing code\" + Starting Expert Mode failed Tap to set up again. Try ADB pairing and rebooting your phone if it repeatedly fails. From a37c5bd148e5dd8a234c99832945e4e1aade57e5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 17 Jan 2026 21:44:08 +0100 Subject: [PATCH 32/48] fix: network adapter does not check network state in a way that there are race conditions --- .../system/network/AndroidNetworkAdapter.kt | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 05393a900a..78545c412b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -82,12 +82,20 @@ class AndroidNetworkAdapter @Inject constructor( ) ?: return connectedWifiSSIDFlow.update { getWifiSSID() } + isWifiConnected.update { networkInfo.isConnected } } } } } + /** + * Store the network handles for connected wifi transports because you must not query + * the list of all network capabilities from the callback. + */ + @RequiresApi(Build.VERSION_CODES.S) + private val wifiTransportHandles: MutableSet = mutableSetOf() + override val connectedWifiSSIDFlow = MutableStateFlow(getWifiSSID()) override val isWifiConnected: MutableStateFlow = MutableStateFlow(getIsWifiConnected()) @@ -98,20 +106,11 @@ class AndroidNetworkAdapter @Inject constructor( private val networkCallback: ConnectivityManager.NetworkCallback by lazy { @RequiresApi(Build.VERSION_CODES.S) object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { - override fun onAvailable(network: Network) { - super.onAvailable(network) - - isWifiConnected.update { getIsWifiConnected() } - } - override fun onLost(network: Network) { super.onLost(network) - // A network was lost. Check if we are still connected to *any* Wi-Fi. - // This is important because onLost is called for a specific network. - // If multiple Wi-Fi networks were available and one is lost, - // another might still be active. + + wifiTransportHandles.remove(network.networkHandle) isWifiConnected.update { getIsWifiConnected() } - connectedWifiSSIDFlow.update { getWifiSSID() } } override fun onCapabilitiesChanged( @@ -120,6 +119,14 @@ class AndroidNetworkAdapter @Inject constructor( ) { super.onCapabilitiesChanged(network, networkCapabilities) + val hasWifiTransport = networkCapabilities.hasTransport( + NetworkCapabilities.TRANSPORT_WIFI, + ) + + if (hasWifiTransport) { + wifiTransportHandles.add(network.networkHandle) + } + isWifiConnected.update { getIsWifiConnected() } val wifiInfo = networkCapabilities.transportInfo as? WifiInfo @@ -318,6 +325,10 @@ class AndroidNetworkAdapter @Inject constructor( } private fun getIsWifiConnected(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return wifiTransportHandles.isNotEmpty() + } + @Suppress("DEPRECATION") // The deprecation notice is advice to use the callback instead. getAllNetworks() still // functions From 9dd14a3cb28ef2af8f8e1d55e860b337055f7fab Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 12:56:21 +0100 Subject: [PATCH 33/48] log unix timestamp in system bridge auto starter --- .../sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt index 411e87548c..0434645c0c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt @@ -157,7 +157,7 @@ class SystemBridgeAutoStarter @Inject constructor( fun init() { coroutineScope.launch { Timber.i( - "SystemBridgeAutoStarter init: time since boot=${clock.elapsedRealtime() / 1000} seconds", + "SystemBridgeAutoStarter init: time since boot=${clock.elapsedRealtime() / 1000} seconds. unix timestamp=${clock.unixTimestamp()}", ) if (BuildConfig.DEBUG && connectionManager.isConnected()) { From fd3fcecd3ef66b2eb600b03b596fe9f378303707 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 13:03:42 +0100 Subject: [PATCH 34/48] set adb command result log level to debug --- .../java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt index dbc48555be..43a1956e44 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt @@ -69,7 +69,7 @@ class AdbManagerImpl @Inject constructor(@ApplicationContext private val ctx: Co } } - Timber.i("Execute command result: $result") + Timber.d("Execute command result: $result") return result } From 367c3c95b54d6da08dcab535362b607daa026097 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 13:13:42 +0100 Subject: [PATCH 35/48] log more reasons why system bridge auto starting fails --- .../expertmode/SystemBridgeAutoStarter.kt | 81 +++++++++++++------ 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt index 0434645c0c..14be776f2a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt @@ -81,7 +81,14 @@ class SystemBridgeAutoStarter @Inject constructor( sealed class NotEligible : AutoStartEligibility() { data object WiFiDisconnected : NotEligible() data object AutoStartCooldown : NotEligible() - data object Other : NotEligible() + data object WriteSecureSettingsRevoked : NotEligible() + data object AdbUnpaired : NotEligible() + data object ShizukuRootRequired : NotEligible() + data object SystemBridgeStoppedByUser : NotEligible() + data object NotUsedBefore : NotEligible() + data object EmergencyKilled : NotEligible() + data object AutoStartDisabled : NotEligible() + data object SystemBridgeConnected : NotEligible() } } @@ -112,18 +119,26 @@ class SystemBridgeAutoStarter @Inject constructor( permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), networkAdapter.isWifiConnected, ) { isWriteSecureSettingsGranted, isWifiConnected -> - if (!isWifiConnected) { - AutoStartEligibility.NotEligible.WiFiDisconnected - } else if (!isWriteSecureSettingsGranted) { - AutoStartEligibility.NotEligible.Other - } else if (!setupController.isAdbPaired()) { - AutoStartEligibility.NotEligible.Other - } else { - AutoStartEligibility.Eligible(AutoStartType.ADB) + when { + !isWifiConnected -> { + AutoStartEligibility.NotEligible.WiFiDisconnected + } + + !isWriteSecureSettingsGranted -> { + AutoStartEligibility.NotEligible.WriteSecureSettingsRevoked + } + + !setupController.isAdbPaired() -> { + AutoStartEligibility.NotEligible.AdbUnpaired + } + + else -> { + AutoStartEligibility.Eligible(AutoStartType.ADB) + } } } } else { - flowOf(AutoStartEligibility.NotEligible.Other) + flowOf(AutoStartEligibility.NotEligible.ShizukuRootRequired) } } } @@ -134,20 +149,7 @@ class SystemBridgeAutoStarter @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private val autoStartFlow: Flow = connectionManager.connectionState.flatMapLatest { connectionState -> - // Do not autostart if it is connected or it was killed from the user - if (connectionState !is SystemBridgeConnectionState.Disconnected || - connectionState.isStoppedByUser || - !getIsUsedBefore() || - getIsStoppedByUser() || - isSystemBridgeEmergencyKilled() || - !isAutoStartEnabled() - ) { - flowOf(AutoStartEligibility.NotEligible.Other) - } else if (isWithinAutoStartCooldown()) { - flowOf(AutoStartEligibility.NotEligible.AutoStartCooldown) - } else { - autoStartTypeFlow - } + getAutoStartEligibility(connectionState) } /** @@ -181,6 +183,33 @@ class SystemBridgeAutoStarter @Inject constructor( } } + private suspend fun getAutoStartEligibility( + connectionState: SystemBridgeConnectionState, + ): Flow { + return when { + connectionState !is SystemBridgeConnectionState.Disconnected -> + flowOf(AutoStartEligibility.NotEligible.SystemBridgeConnected) + + connectionState.isStoppedByUser -> + flowOf(AutoStartEligibility.NotEligible.SystemBridgeStoppedByUser) + + !getIsUsedBefore() -> flowOf(AutoStartEligibility.NotEligible.NotUsedBefore) + + getIsStoppedByUser() -> + flowOf(AutoStartEligibility.NotEligible.SystemBridgeStoppedByUser) + + isSystemBridgeEmergencyKilled() -> + flowOf(AutoStartEligibility.NotEligible.EmergencyKilled) + + !isAutoStartEnabled() -> flowOf(AutoStartEligibility.NotEligible.AutoStartDisabled) + + isWithinAutoStartCooldown() -> + flowOf(AutoStartEligibility.NotEligible.AutoStartCooldown) + + else -> autoStartTypeFlow + } + } + private suspend fun processAutoStartEligibility(eligibility: AutoStartEligibility) { when (eligibility) { is AutoStartEligibility.Eligible -> autoStart(eligibility.type) @@ -198,7 +227,9 @@ class SystemBridgeAutoStarter @Inject constructor( AutoStartEligibility.NotEligible.WiFiDisconnected -> showWiFiDisconnectedNotification() - AutoStartEligibility.NotEligible.Other -> {} + else -> { + Timber.w("Not auto starting the system bridge: $eligibility") + } } } From 86dc23eb0c839c486a90a94d454bd31b4c991cdf Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 13:14:38 +0100 Subject: [PATCH 36/48] style: extract long lines into separate method --- .../SystemBridgeSetupAssistantController.kt | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt index dca027700e..d823175d9e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt @@ -325,20 +325,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } else { // Show a notification asking for pairing code straight away if interaction // is disabled. - showNotification( - title = getString( - R.string.expert_mode_setup_notification_pairing_code_not_interactive_title, - ), - text = getString( - R.string.expert_mode_setup_notification_pairing_code_not_interactive_text, - ), - actions = listOf( - KMNotificationAction.RemoteInput.PairingCode to - getString( - R.string.expert_mode_setup_notification_action_input_pairing_code, - ), - ), - ) + nonInteractivePairingCodeNotification() interactionStep = null } @@ -348,6 +335,23 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } } + private fun nonInteractivePairingCodeNotification() { + showNotification( + title = getString( + R.string.expert_mode_setup_notification_pairing_code_not_interactive_title, + ), + text = getString( + R.string.expert_mode_setup_notification_pairing_code_not_interactive_text, + ), + actions = listOf( + KMNotificationAction.RemoteInput.PairingCode to + getString( + R.string.expert_mode_setup_notification_action_input_pairing_code, + ), + ), + ) + } + private fun startInteractionTimeoutJob() { interactionTimeoutJob?.cancel() interactionTimeoutJob = coroutineScope.launch { From bf74cfe2badca6ecbaccef3c68c56d7640c43780 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 15:49:06 +0100 Subject: [PATCH 37/48] #1983 fix: inputting a modifier key and another key as actions through Expert mode applies the correct key character map. --- CHANGELOG.md | 1 + .../base/detection/DetectKeyMapsUseCase.kt | 36 +++++-- .../base/detection/KeyMapAlgorithm.kt | 13 ++- .../BaseAccessibilityServiceController.kt | 1 + .../base/keymaps/KeyMapAlgorithmTest.kt | 99 +++++++++++++++++-- 5 files changed, 129 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 417c9f43b9..80e1212b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - [#1976](https://github.com/keymapperorg/KeyMapper/issues/1976) Panic in Rust system bridge code on some devices. - [#1971](https://github.com/keymapperorg/KeyMapper/issues/1971) Media actions work again in some apps, like YouTube. - [#1961](https://github.com/keymapperorg/KeyMapper/issues/1961) Disabling setup assistant shows a notification asking for pairing code immediately. +- [#1983](https://github.com/keymapperorg/KeyMapper/issues/1983) Inputting a modifier key and another key as actions through Expert mode applies the correct key character map. - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt index befa0d8122..01d28b35b1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt @@ -30,16 +30,22 @@ import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.popup.ToastAdapter import io.github.sds100.keymapper.system.vibrator.VibratorAdapter import io.github.sds100.keymapper.system.volume.VolumeAdapter +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import timber.log.Timber class DetectKeyMapsUseCaseImpl @AssistedInject constructor( @Assisted private val accessibilityService: IAccessibilityService, + @Assisted + private val coroutineScope: CoroutineScope, private val keyMapRepository: KeyMapRepository, private val floatingButtonRepository: FloatingButtonRepository, private val groupRepository: GroupRepository, @@ -53,7 +59,10 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(accessibilityService: IAccessibilityService): DetectKeyMapsUseCaseImpl + fun create( + accessibilityService: IAccessibilityService, + coroutineScope: CoroutineScope, + ): DetectKeyMapsUseCaseImpl } companion object { @@ -161,6 +170,11 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( .map { it ?: PreferenceDefaults.VIBRATION_DURATION } .map { it.toLong() } + override val injectKeyEventsWithSystemBridge: StateFlow = + preferenceRepository.get(Keys.keyEventActionsUseSystemBridge) + .map { it ?: PreferenceDefaults.KEY_EVENT_ACTIONS_USE_SYSTEM_BRIDGE } + .stateIn(coroutineScope, SharingStarted.Eagerly, false) + override fun showTriggeredToast() { toastAdapter.show(resourceProvider.getString(R.string.toast_triggered_keymap)) } @@ -188,16 +202,20 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( if (inputEventHub.isSystemBridgeConnected()) { Timber.d( - "Imitate button press ${KeyEvent.keyCodeToString( - keyCode, - )} with system bridge, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode", + "Imitate button press ${ + KeyEvent.keyCodeToString( + keyCode, + ) + } with system bridge, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode", ) inputEventHub.injectKeyEventAsync(model) } else { Timber.d( - "Imitate button press ${KeyEvent.keyCodeToString( - keyCode, - )}, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode", + "Imitate button press ${ + KeyEvent.keyCodeToString( + keyCode, + ) + }, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode", ) when (keyCode) { @@ -208,9 +226,11 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( KeyEvent.KEYCODE_BACK -> accessibilityService.doGlobalAction( AccessibilityService.GLOBAL_ACTION_BACK, ) + KeyEvent.KEYCODE_HOME -> accessibilityService.doGlobalAction( AccessibilityService.GLOBAL_ACTION_HOME, ) + KeyEvent.KEYCODE_APP_SWITCH -> accessibilityService.doGlobalAction( AccessibilityService.GLOBAL_ACTION_POWER_DIALOG, ) @@ -263,4 +283,6 @@ interface DetectKeyMapsUseCase { ) fun imitateEvdevEvent(deviceId: Int, type: Int, code: Int, value: Int) + + val injectKeyEventsWithSystemBridge: StateFlow } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index b2b137abbe..5cc9f663ff 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -910,10 +910,15 @@ class KeyMapAlgorithm( if (overlappingSequenceTrigger == null) { val actionKeys = triggerActions[triggerIndex] - actionKeys.forEach { actionKey -> - val action = actionMap[actionKey] ?: return@forEach + for (actionKey in actionKeys) { + val action = actionMap[actionKey] ?: continue - if (action.data is ActionData.InputKeyEvent) { + // If the key event is being injected with the system bridge + // then it will be passed back around through the accessibility + // service and processed again. + if (action.data is ActionData.InputKeyEvent && + !useCase.injectKeyEventsWithSystemBridge.value + ) { val actionKeyCode = action.data.keyCode if (isModifierKey(actionKeyCode)) { @@ -1073,7 +1078,6 @@ class KeyMapAlgorithm( key.matchesEvent(event.withShortPress) -> true key.matchesEvent(event.withLongPress) -> true key.matchesEvent(event.withDoublePress) -> true - else -> false } @@ -1794,6 +1798,7 @@ class KeyMapAlgorithm( return when (this.device) { KeyEventTriggerDevice.Any -> codeMatches && this.clickType == event.clickType + is KeyEventTriggerDevice.External -> event.isExternal && codeMatches && diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 5f36cb46f9..a713408d13 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -82,6 +82,7 @@ abstract class BaseAccessibilityServiceController( private val detectKeyMapsUseCase = detectKeyMapsUseCaseFactory.create( accessibilityService = service, + coroutineScope = service.lifecycleScope, ) val detectConstraintsUseCase = detectConstraintsUseCaseFactory.create(service) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 8a066f8581..52cc168446 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -193,6 +193,9 @@ class KeyMapAlgorithmTest { } whenever(detectKeyMapsUseCase.currentTime).thenAnswer { testScope.currentTime } + whenever( + detectKeyMapsUseCase.injectKeyEventsWithSystemBridge, + ).thenReturn(MutableStateFlow(false)) performActionsUseCase = mock { MutableStateFlow(REPEAT_DELAY).apply { @@ -235,6 +238,80 @@ class KeyMapAlgorithmTest { mockedKeyEvent.close() } + /** + * Issue #1983 + */ + @Test + fun `do not imitate keys with meta state when injecting key event actions with system bridge`() = + runTest(testDispatcher) { + whenever( + detectKeyMapsUseCase.injectKeyEventsWithSystemBridge, + ).thenReturn(MutableStateFlow(true)) + + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_S, + scanCode = Scancode.KEY_S, + device = FAKE_VOLUME_EVDEV_DEVICE, + ), + ) + + val actions = listOf( + Action( + data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT), + holdDown = true, + ), + Action( + data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_3), + holdDown = true, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = actions)) + + inputDownEvdevEvent( + KeyEvent.KEYCODE_S, + Scancode.KEY_S, + device = FAKE_VOLUME_EVDEV_DEVICE, + ) + + // Simulate the SHIFT and 3 being reinputted from the grabbed evdev device + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_SHIFT_LEFT, + action = KeyEvent.ACTION_DOWN, + metaState = + KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON, + ) + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_3, + action = KeyEvent.ACTION_DOWN, + metaState = + KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON, + ) + + inputUpEvdevEvent( + KeyEvent.KEYCODE_S, + Scancode.KEY_S, + device = FAKE_VOLUME_EVDEV_DEVICE, + ) + + // Simulate the SHIFT and 3 being reinputted from the grabbed evdev device + inputKeyEvent(keyCode = KeyEvent.KEYCODE_SHIFT_LEFT, action = KeyEvent.ACTION_UP) + inputKeyEvent(keyCode = KeyEvent.KEYCODE_3, action = KeyEvent.ACTION_UP) + + verify( + detectKeyMapsUseCase, + never(), + ).imitateKeyEvent( + keyCode = any(), + metaState = any(), + deviceId = any(), + action = any(), + scanCode = any(), + source = any(), + ) + } + @Test fun `Detect mouse button which only has scan code`() = runTest(testDispatcher) { val trigger = singleKeyTrigger( @@ -1832,16 +1909,18 @@ class KeyMapAlgorithmTest { loadKeyMaps(KeyMap(trigger = trigger, actionList = actionList)) // WHEN - whenever(performActionsUseCase.getErrorSnapshot()).thenReturn(object : - ActionErrorSnapshot { - override fun getError(action: ActionData): KMError { - return KMError.NoCompatibleImeChosen - } - - override fun getErrors(actions: List): Map { - return mapOf(actionList[0].data to KMError.NoCompatibleImeChosen) - } - }) + whenever(performActionsUseCase.getErrorSnapshot()).thenReturn( + object : + ActionErrorSnapshot { + override fun getError(action: ActionData): KMError { + return KMError.NoCompatibleImeChosen + } + + override fun getErrors(actions: List): Map { + return mapOf(actionList[0].data to KMError.NoCompatibleImeChosen) + } + }, + ) assertThat( inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), From 9c9dcaf997b293ccbd577e4a8a10457a3e68f8ce Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 16:11:47 +0100 Subject: [PATCH 38/48] fix: choosing key mapper input method does not launch IME settings screen if GUI keyboard also installed --- .../keymapper/base/keymaps/DisplayKeyMapUseCase.kt | 6 +++--- .../keymapper/base/settings/ConfigSettingsUseCase.kt | 2 +- .../base/system/inputmethod/KeyMapperImeHelper.kt | 8 +++++++- .../base/system/inputmethod/SwitchImeAsyncImpl.kt | 10 ++++++---- .../keymapper/base/trigger/SetupInputMethodUseCase.kt | 4 ++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt index b21b126d86..6bf7f2ca6a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt @@ -212,7 +212,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( inputMethodAdapter.showImePicker(fromForeground = true) } - KMError.NoCompatibleImeEnabled -> keyMapperImeHelper.enableCompatibleInputMethods() + KMError.NoCompatibleImeEnabled -> keyMapperImeHelper.enableCompatibleInputMethod() is ImeDisabled -> switchImeInterface.enableIme(error.ime.id) @@ -224,7 +224,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( is KMError.CantDetectKeyEventsInPhoneCall -> { if (!keyMapperImeHelper.isCompatibleImeEnabled()) { - keyMapperImeHelper.enableCompatibleInputMethods() + keyMapperImeHelper.enableCompatibleInputMethod() } // wait for compatible ime to be enabled then choose it. @@ -244,7 +244,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( if (keyMapperImeHelper.isCompatibleImeEnabled()) { keyMapperImeHelper.chooseCompatibleInputMethod() } else { - keyMapperImeHelper.enableCompatibleInputMethods() + keyMapperImeHelper.enableCompatibleInputMethod() } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index ddf8d4ce88..306571de8c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -102,7 +102,7 @@ class ConfigSettingsUseCaseImpl @Inject constructor( get() = devicesAdapter.connectedInputDevices override suspend fun enableCompatibleIme() { - imeHelper.enableCompatibleInputMethods() + imeHelper.enableCompatibleInputMethod() } override suspend fun chooseCompatibleIme(): KMResult = diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/KeyMapperImeHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/KeyMapperImeHelper.kt index 290ab90d19..72c585cd23 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/KeyMapperImeHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/KeyMapperImeHelper.kt @@ -4,6 +4,7 @@ import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.firstBlocking +import io.github.sds100.keymapper.common.utils.isSuccess import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.common.utils.valueOrNull @@ -58,7 +59,7 @@ class KeyMapperImeHelper( } } - fun enableCompatibleInputMethods(): KMResult { + fun enableCompatibleInputMethod(): KMResult { var result: KMResult? = null for (imePackageName in keyMapperImePackageList) { @@ -66,6 +67,11 @@ class KeyMapperImeHelper( imeAdapter.getInfoByPackageName(imePackageName).valueOrNull()?.id ?: continue result = switchImeInterface.enableIme(imeId) + + // Stop trying to enable IMEs if one is enabled. + if (result.isSuccess) { + break + } } return result ?: KMError.InputMethodNotFound(packageName) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/SwitchImeAsyncImpl.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/SwitchImeAsyncImpl.kt index 2f41dbab76..a1659c9920 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/SwitchImeAsyncImpl.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/SwitchImeAsyncImpl.kt @@ -55,14 +55,16 @@ class SwitchImeAsyncImpl @Inject constructor( } private fun enableImeWithoutUserInput(imeId: String): KMResult { - return inputMethodAdapter.getInfoByPackageName(buildConfigProvider.packageName) - .then { keyMapperImeInfo -> + return inputMethodAdapter.getInfoById(imeId) + .then { imeInfo -> + // The accessibility service can only enable IMEs that have the same + // package name as the accessibility service. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - imeId == keyMapperImeInfo.id + imeInfo.packageName == buildConfigProvider.packageName ) { serviceAdapter.sendAsync( AccessibilityServiceEvent.EnableInputMethod( - keyMapperImeInfo.id, + imeInfo.id, ), ) } else { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/SetupInputMethodUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/SetupInputMethodUseCase.kt index c5e3fbfd90..2079b9d3de 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/SetupInputMethodUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/SetupInputMethodUseCase.kt @@ -29,7 +29,7 @@ class SetupInputMethodUseCaseImpl @Inject constructor( override val isEnabled: Flow = keyMapperImeHelper.isCompatibleImeEnabledFlow override suspend fun enableInputMethod(): KMResult { - return keyMapperImeHelper.enableCompatibleInputMethods() + return keyMapperImeHelper.enableCompatibleInputMethod() } override val isChosen: Flow = keyMapperImeHelper.isCompatibleImeChosenFlow @@ -38,7 +38,7 @@ class SetupInputMethodUseCaseImpl @Inject constructor( // On Android 13+, the accessibility service can enable the input method without // any user input if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return keyMapperImeHelper.enableCompatibleInputMethods() + return keyMapperImeHelper.enableCompatibleInputMethod() .onFailure { Timber.e("Failed to enable compatible input method: $it") } From da0b7db0661dc9b24e90c65c2476985d744ef040 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 16:23:21 +0100 Subject: [PATCH 39/48] #1990 fix: Passthrough the device id of the trigger to the key event action if one is not manually specified --- CHANGELOG.md | 1 + .../actions/PerformActionTriggerDevice.kt | 5 ++++ .../actions/PerformKeyEventActionDelegate.kt | 12 ++++++-- .../base/detection/KeyMapAlgorithm.kt | 8 +++-- .../PerformKeyEventActionDelegateTest.kt | 30 +++++++++++++++++++ 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e1212b06..b4220b9bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - [#1971](https://github.com/keymapperorg/KeyMapper/issues/1971) Media actions work again in some apps, like YouTube. - [#1961](https://github.com/keymapperorg/KeyMapper/issues/1961) Disabling setup assistant shows a notification asking for pairing code immediately. - [#1983](https://github.com/keymapperorg/KeyMapper/issues/1983) Inputting a modifier key and another key as actions through Expert mode applies the correct key character map. +- [#1990](https://github.com/keymapperorg/KeyMapper/issues/1990) Passthrough the device id of the trigger to the key event action if one is not manually specified - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionTriggerDevice.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionTriggerDevice.kt index f478803eba..2d2f93d881 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionTriggerDevice.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionTriggerDevice.kt @@ -9,5 +9,10 @@ sealed class PerformActionTriggerDevice { */ data class Evdev(val deviceId: Int) : PerformActionTriggerDevice() + /** + * The action was triggered by an Android InputDevice. + */ + data class AndroidDevice(val deviceId: Int) : PerformActionTriggerDevice() + data object Default : PerformActionTriggerDevice() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegate.kt index a059059ec3..d57a8e4844 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegate.kt @@ -47,7 +47,7 @@ class PerformKeyEventActionDelegate( return injectEvdevEvent(inputEventAction, triggerDevice.deviceId, action) } - val deviceId: Int = getDeviceIdForKeyEventAction(action) + val deviceId: Int = getAndroidDeviceIdForKeyEventAction(triggerDevice, action) // If the device that the user specified in the action can not be found // then fallback to evdev injection. @@ -157,7 +157,10 @@ class PerformKeyEventActionDelegate( } } - private fun getDeviceIdForKeyEventAction(action: ActionData.InputKeyEvent): Int { + private fun getAndroidDeviceIdForKeyEventAction( + triggerDevice: PerformActionTriggerDevice, + action: ActionData.InputKeyEvent, + ): Int { if (action.device?.descriptor == null) { // automatically select a game controller as the input device for game controller key events @@ -171,7 +174,10 @@ class PerformKeyEventActionDelegate( } } - return 0 + return when (triggerDevice) { + is PerformActionTriggerDevice.AndroidDevice -> triggerDevice.deviceId + else -> 0 + } } val inputDevices = devicesAdapter.connectedInputDevices.value diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index 5cc9f663ff..a895c2b4c2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -287,7 +287,6 @@ class KeyMapAlgorithm( val triggerActions = mutableListOf() val triggerConstraints = mutableListOf>() - val triggerPerformActionDevices = mutableListOf() val sequenceTriggerActionPerformers = mutableMapOf() @@ -1995,8 +1994,11 @@ class KeyMapAlgorithm( private fun AlgoEvent.performActionDevice(): PerformActionTriggerDevice { return when (this) { - is EvdevEventAlgo -> PerformActionTriggerDevice.Evdev(deviceId) - else -> PerformActionTriggerDevice.Default + is EvdevEventAlgo -> PerformActionTriggerDevice.Evdev(this.deviceId) + is KeyEventAlgo -> PerformActionTriggerDevice.AndroidDevice(this.deviceId) + is AssistantEvent -> PerformActionTriggerDevice.Default + is FingerprintGestureEvent -> PerformActionTriggerDevice.Default + is FloatingButtonEvent -> PerformActionTriggerDevice.Default } } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegateTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegateTest.kt index 9cb56943b0..e3dcccf226 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegateTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegateTest.kt @@ -68,6 +68,36 @@ class PerformKeyEventActionDelegateTest { ) } + @Test + fun `use trigger device id if no device specified for action`() = runTest(testDispatcher) { + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_A, + device = null, + ) + + delegate.perform( + action, + inputEventAction = InputEventAction.DOWN, + keyMetaState = 0, + triggerDevice = PerformActionTriggerDevice.AndroidDevice(deviceId = 3), + ) + + val expectedDownEvent = InjectKeyEventModel( + keyCode = KeyEvent.KEYCODE_A, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = 3, + scanCode = 0, + repeatCount = 0, + source = InputDevice.SOURCE_KEYBOARD, + ) + + verify(mockInputEventHub).injectKeyEvent( + expectedDownEvent, + useSystemBridgeIfAvailable = false, + ) + } + @Test fun `inject evdev event if action device set as a non-evdev device but it is disconnected`() = runTest(testDispatcher) { From 863b7c62e830de8503e1d326cc6c276ce971bc02 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 16:35:15 +0100 Subject: [PATCH 40/48] #1982 feat: text action does not need Key Mapper input method on Android 13+ --- CHANGELOG.md | 1 + .../github/sds100/keymapper/base/actions/ActionUtils.kt | 2 +- .../keymapper/base/actions/PerformActionsUseCase.kt | 6 +++++- .../system/accessibility/BaseAccessibilityService.kt | 9 +++++++++ .../base/system/accessibility/IAccessibilityService.kt | 5 +++++ 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4220b9bf0..6a0b15a915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - [#1961](https://github.com/keymapperorg/KeyMapper/issues/1961) Disabling setup assistant shows a notification asking for pairing code immediately. - [#1983](https://github.com/keymapperorg/KeyMapper/issues/1983) Inputting a modifier key and another key as actions through Expert mode applies the correct key character map. - [#1990](https://github.com/keymapperorg/KeyMapper/issues/1990) Passthrough the device id of the trigger to the key event action if one is not manually specified +- [#1982](https://github.com/keymapperorg/KeyMapper/issues/1982) Text action does not need Key Mapper input method on Android 13+. - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) 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 9bd390fccc..e42009f9b8 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 @@ -1060,7 +1060,7 @@ fun ActionData.canBeHeldDown(): Boolean = when (this) { fun ActionData.canUseImeToPerform(): Boolean = when (this) { is ActionData.InputKeyEvent -> true - is ActionData.Text -> true + is ActionData.Text -> Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU else -> false } 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 77171ed65a..b72a058aac 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 @@ -375,7 +375,11 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.Text -> { - keyMapperImeMessenger.inputText(action.text) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + service.injectText(action.text) + } else { + keyMapperImeMessenger.inputText(action.text) + } result = Success(Unit) } 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 ad8ea69ca2..ccb7d38ff1 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 @@ -560,4 +560,13 @@ abstract class BaseAccessibilityService : return imeWindow != null && imeWindow.root?.isVisibleToUser == true } + + override fun injectText(text: String) { + inputMethod?.currentInputConnection?.commitText( + text, + // 1 puts the cursor after the inserted text. + 1, + null, + ) + } } 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 9894cbee3f..3eaa73ca40 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 @@ -1,5 +1,7 @@ package io.github.sds100.keymapper.base.system.accessibility +import android.os.Build +import androidx.annotation.RequiresApi 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 @@ -61,4 +63,7 @@ interface IAccessibilityService : SwitchImeInterface { val isInputMethodVisible: Flow fun findFocussedNode(focus: Int): AccessibilityNodeModel? + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + fun injectText(text: String) } From e21ccff91ee4962ccdb35d4018a2c085c48585db Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 16:58:45 +0100 Subject: [PATCH 41/48] #1989 center the "Trigger and actions" and "Constraint and more" tabs --- CHANGELOG.md | 1 + .../base/keymaps/BaseConfigKeyMapScreen.kt | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0b15a915..6ab6100cac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - [#1983](https://github.com/keymapperorg/KeyMapper/issues/1983) Inputting a modifier key and another key as actions through Expert mode applies the correct key character map. - [#1990](https://github.com/keymapperorg/KeyMapper/issues/1990) Passthrough the device id of the trigger to the key event action if one is not manually specified - [#1982](https://github.com/keymapperorg/KeyMapper/issues/1982) Text action does not need Key Mapper input method on Android 13+. +- [#1989](https://github.com/keymapperorg/KeyMapper/issues/1989) center the "Trigger and actions" and "Constraint and more" tabs. - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt index 62ab1dfaff..67ae276bcd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.displayCutoutPadding @@ -32,7 +33,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.PrimaryScrollableTabRow -import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -200,19 +200,24 @@ fun BaseConfigKeyMapScreen( } } - if (this@BoxWithConstraints.maxWidth < 500.dp) { + if (tabs.size == 2) { + // If only two tabs are showing then center them PrimaryScrollableTabRow( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .width(IntrinsicSize.Max), + edgePadding = 0.dp, selectedTabIndex = pagerState.targetPage, divider = {}, - edgePadding = 16.dp, contentColor = MaterialTheme.colorScheme.onSurface, ) { Tabs() } } else { - PrimaryTabRow( + PrimaryScrollableTabRow( selectedTabIndex = pagerState.targetPage, divider = {}, + edgePadding = 16.dp, contentColor = MaterialTheme.colorScheme.onSurface, ) { Tabs() @@ -226,9 +231,13 @@ fun BaseConfigKeyMapScreen( ) { pageIndex -> when (tabs[pageIndex]) { ConfigKeyMapTab.TRIGGER -> triggerScreen() + ConfigKeyMapTab.ACTIONS -> actionsScreen() + ConfigKeyMapTab.CONSTRAINTS -> constraintsScreen() + ConfigKeyMapTab.OPTIONS -> optionsScreen() + ConfigKeyMapTab.TRIGGER_AND_ACTIONS -> { if (isVerticalTwoScreen) { VerticalTwoScreens( From 7d7ddc0a3680575876b8d2c2ed4ddee7cda11a27 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 17:16:18 +0100 Subject: [PATCH 42/48] #1392 feat: add action to enable/disable/toggle night shift --- CHANGELOG.md | 1 + .../keymapper/base/actions/ActionData.kt | 18 +++++++ .../base/actions/ActionDataEntityMapper.kt | 8 +++ .../sds100/keymapper/base/actions/ActionId.kt | 4 ++ .../keymapper/base/actions/ActionUiHelper.kt | 4 ++ .../keymapper/base/actions/ActionUtils.kt | 23 +++++++++ .../base/actions/CreateActionDelegate.kt | 6 +++ .../base/actions/PerformActionsUseCase.kt | 49 +++++++++++++++++-- base/src/main/res/values/strings.xml | 3 ++ 9 files changed, 113 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab6100cac..3126d75ced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - [#1990](https://github.com/keymapperorg/KeyMapper/issues/1990) Passthrough the device id of the trigger to the key event action if one is not manually specified - [#1982](https://github.com/keymapperorg/KeyMapper/issues/1982) Text action does not need Key Mapper input method on Android 13+. - [#1989](https://github.com/keymapperorg/KeyMapper/issues/1989) center the "Trigger and actions" and "Constraint and more" tabs. +- [#1392](https://github.com/keymapperorg/KeyMapper/issues/1392) Add action to enable/disable/toggle night shift. - Bugs with expert mode auto starting time. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) 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 bb92e9bec0..eb24dd579a 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 @@ -714,6 +714,24 @@ sealed class ActionData : Comparable { } } + @Serializable + sealed class NightShift : ActionData() { + @Serializable + data object Enable : NightShift() { + override val id = ActionId.ENABLE_NIGHT_SHIFT + } + + @Serializable + data object Disable : NightShift() { + override val id = ActionId.DISABLE_NIGHT_SHIFT + } + + @Serializable + data object Toggle : NightShift() { + override val id = ActionId.TOGGLE_NIGHT_SHIFT + } + } + @Serializable sealed class StatusBar : ActionData() { @Serializable 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 8cc75755ef..a4485f9813 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 @@ -498,6 +498,10 @@ object ActionDataEntityMapper { ActionId.INCREASE_BRIGHTNESS -> ActionData.Brightness.Increase ActionId.DECREASE_BRIGHTNESS -> ActionData.Brightness.Decrease + ActionId.TOGGLE_NIGHT_SHIFT -> ActionData.NightShift.Toggle + ActionId.ENABLE_NIGHT_SHIFT -> ActionData.NightShift.Enable + ActionId.DISABLE_NIGHT_SHIFT -> ActionData.NightShift.Disable + ActionId.TOGGLE_AUTO_ROTATE -> ActionData.Rotation.ToggleAuto ActionId.ENABLE_AUTO_ROTATE -> ActionData.Rotation.EnableAuto ActionId.DISABLE_AUTO_ROTATE -> ActionData.Rotation.DisableAuto @@ -1238,6 +1242,10 @@ object ActionDataEntityMapper { ActionId.INCREASE_BRIGHTNESS to "increase_brightness", ActionId.DECREASE_BRIGHTNESS to "decrease_brightness", + ActionId.TOGGLE_NIGHT_SHIFT to "toggle_night_shift", + ActionId.ENABLE_NIGHT_SHIFT to "enable_night_shift", + ActionId.DISABLE_NIGHT_SHIFT to "disable_night_shift", + ActionId.TOGGLE_AUTO_ROTATE to "toggle_auto_rotate", ActionId.ENABLE_AUTO_ROTATE to "enable_auto_rotate", ActionId.DISABLE_AUTO_ROTATE to "disable_auto_rotate", 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 ebcc6fa9d9..70cbf87a59 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 @@ -38,6 +38,10 @@ enum class ActionId { INCREASE_BRIGHTNESS, DECREASE_BRIGHTNESS, + TOGGLE_NIGHT_SHIFT, + ENABLE_NIGHT_SHIFT, + DISABLE_NIGHT_SHIFT, + TOGGLE_AUTO_ROTATE, ENABLE_AUTO_ROTATE, DISABLE_AUTO_ROTATE, 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 57b09e086f..98b53064b4 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 @@ -475,6 +475,10 @@ class ActionUiHelper( 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) 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 e42009f9b8..14660ca1b4 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 @@ -45,6 +45,7 @@ import androidx.compose.material.icons.outlined.Mic import androidx.compose.material.icons.outlined.MicOff import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.Nfc +import androidx.compose.material.icons.outlined.Nightlight import androidx.compose.material.icons.outlined.NotStarted import androidx.compose.material.icons.outlined.Pause import androidx.compose.material.icons.outlined.PhonelinkRing @@ -148,6 +149,9 @@ object ActionUtils { ActionId.ENABLE_AUTO_BRIGHTNESS -> ActionCategory.DISPLAY ActionId.INCREASE_BRIGHTNESS -> ActionCategory.DISPLAY ActionId.DECREASE_BRIGHTNESS -> ActionCategory.DISPLAY + ActionId.TOGGLE_NIGHT_SHIFT -> ActionCategory.DISPLAY + ActionId.ENABLE_NIGHT_SHIFT -> ActionCategory.DISPLAY + ActionId.DISABLE_NIGHT_SHIFT -> ActionCategory.DISPLAY ActionId.SCREENSHOT -> ActionCategory.DISPLAY ActionId.TOGGLE_AUTO_ROTATE -> ActionCategory.INTERFACE ActionId.ENABLE_AUTO_ROTATE -> ActionCategory.INTERFACE @@ -275,6 +279,12 @@ object ActionUtils { ActionId.DECREASE_BRIGHTNESS -> R.string.action_decrease_brightness + ActionId.TOGGLE_NIGHT_SHIFT -> R.string.action_toggle_night_shift + + ActionId.ENABLE_NIGHT_SHIFT -> R.string.action_enable_night_shift + + ActionId.DISABLE_NIGHT_SHIFT -> R.string.action_disable_night_shift + ActionId.TOGGLE_AUTO_ROTATE -> R.string.action_toggle_auto_rotate ActionId.ENABLE_AUTO_ROTATE -> R.string.action_enable_auto_rotate @@ -681,6 +691,11 @@ object ActionUtils { ActionId.DISABLE_HOTSPOT, -> Build.VERSION_CODES.R + ActionId.TOGGLE_NIGHT_SHIFT, + ActionId.ENABLE_NIGHT_SHIFT, + ActionId.DISABLE_NIGHT_SHIFT, + -> Build.VERSION_CODES.Q + else -> Constants.MIN_API } @@ -840,6 +855,11 @@ object ActionUtils { ActionId.DECREASE_BRIGHTNESS, -> return listOf(Permission.WRITE_SETTINGS) + ActionId.TOGGLE_NIGHT_SHIFT, + ActionId.ENABLE_NIGHT_SHIFT, + ActionId.DISABLE_NIGHT_SHIFT, + -> return listOf(Permission.WRITE_SECURE_SETTINGS) + ActionId.TOGGLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.DISABLE_FLASHLIGHT, @@ -935,6 +955,9 @@ object ActionUtils { ActionId.ENABLE_AUTO_BRIGHTNESS -> Icons.Outlined.BrightnessAuto ActionId.INCREASE_BRIGHTNESS -> Icons.Outlined.BrightnessHigh ActionId.DECREASE_BRIGHTNESS -> Icons.Outlined.BrightnessLow + ActionId.TOGGLE_NIGHT_SHIFT -> Icons.Outlined.Nightlight + ActionId.ENABLE_NIGHT_SHIFT -> Icons.Outlined.Nightlight + ActionId.DISABLE_NIGHT_SHIFT -> Icons.Outlined.Nightlight ActionId.TOGGLE_AUTO_ROTATE -> Icons.Outlined.ScreenRotation ActionId.ENABLE_AUTO_ROTATE -> Icons.Outlined.ScreenRotation ActionId.DISABLE_AUTO_ROTATE -> Icons.Outlined.ScreenLockRotation 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 39290b9595..ded1fa8050 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 @@ -986,6 +986,12 @@ class CreateActionDelegate( ActionId.DECREASE_BRIGHTNESS -> return ActionData.Brightness.Decrease + ActionId.TOGGLE_NIGHT_SHIFT -> return ActionData.NightShift.Toggle + + ActionId.ENABLE_NIGHT_SHIFT -> return ActionData.NightShift.Enable + + ActionId.DISABLE_NIGHT_SHIFT -> return ActionData.NightShift.Disable + ActionId.TOGGLE_AUTO_ROTATE -> return ActionData.Rotation.ToggleAuto ActionId.ENABLE_AUTO_ROTATE -> return ActionData.Rotation.EnableAuto 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 b72a058aac..2cb5589e1f 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 @@ -65,6 +65,8 @@ import io.github.sds100.keymapper.system.phone.PhoneAdapter import io.github.sds100.keymapper.system.popup.ToastAdapter import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.system.settings.SettingType +import io.github.sds100.keymapper.system.settings.SettingsAdapter import io.github.sds100.keymapper.system.shell.ShellAdapter import io.github.sds100.keymapper.system.url.OpenUrlAdapter import io.github.sds100.keymapper.system.volume.RingerMode @@ -118,9 +120,13 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( private val settingsRepository: PreferenceRepository, private val inputEventHub: InputEventHub, private val systemBridgeConnectionManager: SystemBridgeConnectionManager, - private val settingsAdapter: io.github.sds100.keymapper.system.settings.SettingsAdapter, + private val settingsAdapter: SettingsAdapter, ) : PerformActionsUseCase { + companion object { + private const val SETTING_NIGHT_DISPLAY_ACTIVATED = "night_display_activated" + } + @AssistedFactory interface Factory { fun create( @@ -463,11 +469,19 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.Hotspot.Enable -> { - result = networkAdapter.enableHotspot() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + result = networkAdapter.enableHotspot() + } else { + result = SdkVersionTooLow(minSdk = Build.VERSION_CODES.R) + } } is ActionData.Hotspot.Disable -> { - result = networkAdapter.disableHotspot() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + result = networkAdapter.disableHotspot() + } else { + result = SdkVersionTooLow(minSdk = Build.VERSION_CODES.R) + } } is ActionData.Brightness.ToggleAuto -> { @@ -1043,6 +1057,35 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( action.value, ) } + + is ActionData.NightShift.Enable -> { + result = settingsAdapter.setValue( + SettingType.SECURE, + SETTING_NIGHT_DISPLAY_ACTIVATED, + "1", + ) + } + + is ActionData.NightShift.Disable -> { + result = settingsAdapter.setValue( + SettingType.SECURE, + SETTING_NIGHT_DISPLAY_ACTIVATED, + "0", + ) + } + + is ActionData.NightShift.Toggle -> { + val currentValue = settingsAdapter.getValue( + SettingType.SECURE, + SETTING_NIGHT_DISPLAY_ACTIVATED, + ) + val newValue = if (currentValue == "1") "0" else "1" + result = settingsAdapter.setValue( + SettingType.SECURE, + SETTING_NIGHT_DISPLAY_ACTIVATED, + newValue, + ) + } } when (result) { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 18e589fc2b..908ce97d8a 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -961,6 +961,9 @@ Enable auto brightness Increase display brightness Decrease display brightness + Toggle night shift + Enable night shift + Disable night shift Expand notification drawer Toggle notification drawer From 9f4aa6c9e4cec8e41fe666b50b85992970f3c771 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 20:06:22 +0100 Subject: [PATCH 43/48] #1675 feat: option to make floating buttons movable and not locked in position --- CHANGELOG.md | 2 +- .../base/floating/FloatingButtonData.kt | 4 + base/src/main/res/values/strings.xml | 1 + .../22.json | 475 ++++++++++++++++++ .../sds100/keymapper/data/db/AppDatabase.kt | 5 +- .../data/db/dao/FloatingButtonDao.kt | 1 + .../data/entities/FloatingButtonEntity.kt | 8 + .../data/migration/AutoMigration21To22.kt | 5 + 8 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 data/schemas/io.github.sds100.keymapper.data.db.AppDatabase/22.json create mode 100644 data/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration21To22.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3126d75ced..c8c2b8cbd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ - [#1982](https://github.com/keymapperorg/KeyMapper/issues/1982) Text action does not need Key Mapper input method on Android 13+. - [#1989](https://github.com/keymapperorg/KeyMapper/issues/1989) center the "Trigger and actions" and "Constraint and more" tabs. - [#1392](https://github.com/keymapperorg/KeyMapper/issues/1392) Add action to enable/disable/toggle night shift. -- Bugs with expert mode auto starting time. +- [#1675](https://github.com/keymapperorg/KeyMapper/issues/1675) Option to make floating buttons movable ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonData.kt b/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonData.kt index d696dcc7f2..c829e9e8e8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonData.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonData.kt @@ -18,6 +18,7 @@ data class FloatingButtonData( val location: Location, val showOverStatusBar: Boolean, val showOverInputMethod: Boolean, + val isPositionLocked: Boolean, ) { /** * This stores data about where a draggable overlay is located. It needs extra information @@ -83,6 +84,8 @@ object FloatingButtonEntityMapper { ), showOverStatusBar = entity.showOverStatusBar ?: false, showOverInputMethod = entity.showOverInputMethod ?: false, + // Should be locked by default. + isPositionLocked = entity.isPositionLocked ?: true, ) } @@ -101,6 +104,7 @@ object FloatingButtonEntityMapper { displayHeight = button.location.displaySize.height, showOverStatusBar = button.showOverStatusBar, showOverInputMethod = button.showOverInputMethod, + isPositionLocked = button.isPositionLocked, ) } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 908ce97d8a..9b7f23983e 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1437,6 +1437,7 @@ The button must have text! Show over notification panel Show over keyboard + Lock in position Floating buttons Floating buttons display over the apps you want. They work just like real buttons, and you can place, style, and map them however you like. Floating button %s (%s) diff --git a/data/schemas/io.github.sds100.keymapper.data.db.AppDatabase/22.json b/data/schemas/io.github.sds100.keymapper.data.db.AppDatabase/22.json new file mode 100644 index 0000000000..3c040ac43d --- /dev/null +++ b/data/schemas/io.github.sds100.keymapper.data.db.AppDatabase/22.json @@ -0,0 +1,475 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "7b019733eb852154e9d11e5a7cc04d1f", + "entities": [ + { + "tableName": "keymaps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trigger` TEXT NOT NULL, `action_list` TEXT NOT NULL, `constraint_list` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `is_enabled` INTEGER NOT NULL, `uid` TEXT NOT NULL, `group_uid` TEXT, FOREIGN KEY(`group_uid`) REFERENCES `groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trigger", + "columnName": "trigger", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actionList", + "columnName": "action_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraint_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "groupUid", + "columnName": "group_uid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_keymaps_uid", + "unique": true, + "columnNames": [ + "uid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_keymaps_uid` ON `${TABLE_NAME}` (`uid`)" + } + ], + "foreignKeys": [ + { + "table": "groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "group_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "fingerprintmaps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `action_list` TEXT NOT NULL, `constraint_list` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `extras` TEXT NOT NULL, `flags` INTEGER NOT NULL, `is_enabled` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actionList", + "columnName": "action_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraint_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `severity` INTEGER NOT NULL, `message` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "severity", + "columnName": "severity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "floating_layouts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`uid`))", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_floating_layouts_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_floating_layouts_name` ON `${TABLE_NAME}` (`name`)" + } + ] + }, + { + "tableName": "floating_buttons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `layout_uid` TEXT NOT NULL, `text` TEXT NOT NULL, `button_size` INTEGER NOT NULL, `x` INTEGER NOT NULL, `y` INTEGER NOT NULL, `orientation` TEXT NOT NULL, `display_width` INTEGER NOT NULL, `display_height` INTEGER NOT NULL, `border_opacity` REAL, `background_opacity` REAL, `show_over_status_bar` INTEGER, `show_over_input_method` INTEGER, `position_locked` INTEGER, PRIMARY KEY(`uid`), FOREIGN KEY(`layout_uid`) REFERENCES `floating_layouts`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layoutUid", + "columnName": "layout_uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "buttonSize", + "columnName": "button_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "x", + "columnName": "x", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "y", + "columnName": "y", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orientation", + "columnName": "orientation", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayWidth", + "columnName": "display_width", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayHeight", + "columnName": "display_height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "borderOpacity", + "columnName": "border_opacity", + "affinity": "REAL" + }, + { + "fieldPath": "backgroundOpacity", + "columnName": "background_opacity", + "affinity": "REAL" + }, + { + "fieldPath": "showOverStatusBar", + "columnName": "show_over_status_bar", + "affinity": "INTEGER" + }, + { + "fieldPath": "showOverInputMethod", + "columnName": "show_over_input_method", + "affinity": "INTEGER" + }, + { + "fieldPath": "isPositionLocked", + "columnName": "position_locked", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_floating_buttons_layout_uid", + "unique": false, + "columnNames": [ + "layout_uid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_floating_buttons_layout_uid` ON `${TABLE_NAME}` (`layout_uid`)" + } + ], + "foreignKeys": [ + { + "table": "floating_layouts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "layout_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `name` TEXT NOT NULL, `constraints` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `parent_uid` TEXT, `last_opened_date` INTEGER, PRIMARY KEY(`uid`), FOREIGN KEY(`parent_uid`) REFERENCES `groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraints", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentUid", + "columnName": "parent_uid", + "affinity": "TEXT" + }, + { + "fieldPath": "lastOpenedDate", + "columnName": "last_opened_date", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "foreignKeys": [ + { + "table": "groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "accessibility_nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_name` TEXT NOT NULL, `text` TEXT, `content_description` TEXT, `class_name` TEXT, `view_resource_id` TEXT, `unique_id` TEXT, `actions` INTEGER NOT NULL, `interacted` INTEGER NOT NULL DEFAULT false, `tooltip` TEXT DEFAULT NULL, `hint` TEXT DEFAULT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT" + }, + { + "fieldPath": "contentDescription", + "columnName": "content_description", + "affinity": "TEXT" + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT" + }, + { + "fieldPath": "viewResourceId", + "columnName": "view_resource_id", + "affinity": "TEXT" + }, + { + "fieldPath": "uniqueId", + "columnName": "unique_id", + "affinity": "TEXT" + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interacted", + "columnName": "interacted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "tooltip", + "columnName": "tooltip", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "hint", + "columnName": "hint", + "affinity": "TEXT", + "defaultValue": "NULL" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7b019733eb852154e9d11e5a7cc04d1f')" + ] + } +} \ No newline at end of file diff --git a/data/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt b/data/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt index eddf9302b4..3cc6174bc2 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt @@ -34,6 +34,7 @@ import io.github.sds100.keymapper.data.migration.AutoMigration16To17 import io.github.sds100.keymapper.data.migration.AutoMigration18To19 import io.github.sds100.keymapper.data.migration.AutoMigration19To20 import io.github.sds100.keymapper.data.migration.AutoMigration20To21 +import io.github.sds100.keymapper.data.migration.AutoMigration21To22 import io.github.sds100.keymapper.data.migration.Migration10To11 import io.github.sds100.keymapper.data.migration.Migration11To12 import io.github.sds100.keymapper.data.migration.Migration13To14 @@ -67,6 +68,8 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 AutoMigration(from = 19, to = 20, spec = AutoMigration19To20::class), // Adds floating button settings to show over status bar, and show over input method AutoMigration(from = 20, to = 21, spec = AutoMigration20To21::class), + // Adds floating button option to lock in position + AutoMigration(from = 21, to = 22, spec = AutoMigration21To22::class), ], ) @TypeConverters( @@ -79,7 +82,7 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 abstract class AppDatabase : RoomDatabase() { companion object { const val DATABASE_NAME = "key_map_database" - const val DATABASE_VERSION = 21 + const val DATABASE_VERSION = 22 val MIGRATION_1_2 = object : Migration(1, 2) { diff --git a/data/src/main/java/io/github/sds100/keymapper/data/db/dao/FloatingButtonDao.kt b/data/src/main/java/io/github/sds100/keymapper/data/db/dao/FloatingButtonDao.kt index 1161082f34..9b6abcacdd 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/db/dao/FloatingButtonDao.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/db/dao/FloatingButtonDao.kt @@ -27,6 +27,7 @@ interface FloatingButtonDao { const val KEY_BACKGROUND_OPACITY = "background_opacity" const val KEY_SHOW_OVER_STATUS_BAR = "show_over_status_bar" const val KEY_SHOW_OVER_INPUT_METHOD = "show_over_input_method" + const val KEY_POSITION_LOCKED = "position_locked" } @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_UID = (:uid)") diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/FloatingButtonEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/FloatingButtonEntity.kt index 65e62fa465..9040bc8fcf 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/FloatingButtonEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/FloatingButtonEntity.kt @@ -18,6 +18,7 @@ import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao.Companion.KEY_DI import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao.Companion.KEY_DISPLAY_WIDTH import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao.Companion.KEY_LAYOUT_UID import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao.Companion.KEY_ORIENTATION +import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao.Companion.KEY_POSITION_LOCKED import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao.Companion.KEY_SHOW_OVER_INPUT_METHOD import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao.Companion.KEY_SHOW_OVER_STATUS_BAR import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao.Companion.KEY_TEXT @@ -93,6 +94,10 @@ data class FloatingButtonEntity( @SerializedName(NAME_SHOW_OVER_INPUT_METHOD) val showOverInputMethod: Boolean?, + @ColumnInfo(name = KEY_POSITION_LOCKED) + @SerializedName(KEY_POSITION_LOCKED) + val isPositionLocked: Boolean?, + ) : Parcelable { companion object { // DON'T CHANGE THESE. Used for JSON serialization and parsing. @@ -109,6 +114,7 @@ data class FloatingButtonEntity( const val NAME_BACKGROUND_OPACITY = "background_opacity" const val NAME_SHOW_OVER_STATUS_BAR = "show_over_status_bar" const val NAME_SHOW_OVER_INPUT_METHOD = "show_over_input_method" + const val NAME_POSITION_LOCKED = "position_locked" val DESERIALIZER = jsonDeserializer { val uid by it.json.byString(NAME_UID) @@ -124,6 +130,7 @@ data class FloatingButtonEntity( val backgroundOpacity by it.json.byNullableFloat(NAME_BACKGROUND_OPACITY) val showOverStatusBar by it.json.byNullableBool(NAME_SHOW_OVER_STATUS_BAR) val showOverInputMethod by it.json.byNullableBool(NAME_SHOW_OVER_INPUT_METHOD) + val isPositionLocked by it.json.byNullableBool(NAME_POSITION_LOCKED) FloatingButtonEntity( uid = uid, @@ -139,6 +146,7 @@ data class FloatingButtonEntity( backgroundOpacity = backgroundOpacity, showOverStatusBar = showOverStatusBar, showOverInputMethod = showOverInputMethod, + isPositionLocked = isPositionLocked, ) } } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration21To22.kt b/data/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration21To22.kt new file mode 100644 index 0000000000..cd690c0fc4 --- /dev/null +++ b/data/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration21To22.kt @@ -0,0 +1,5 @@ +package io.github.sds100.keymapper.data.migration + +import androidx.room.migration.AutoMigrationSpec + +class AutoMigration21To22 : AutoMigrationSpec From fdad2d36b30cdf7d959b95e726e7d528376d3ff4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 20:29:03 +0100 Subject: [PATCH 44/48] #1949 feat: floating buttons are completely invisible when pressed if background and border opacity is set to 0 --- CHANGELOG.md | 3 ++- .../keymapper/base/home/KeyMapListItemCreator.kt | 15 +++++++++++++++ .../keymapper/base/trigger/TriggerKeyListItem.kt | 12 ++++++++---- base/src/main/res/values/strings.xml | 5 +++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c2b8cbd5..f3c63b86ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,8 @@ - [#1982](https://github.com/keymapperorg/KeyMapper/issues/1982) Text action does not need Key Mapper input method on Android 13+. - [#1989](https://github.com/keymapperorg/KeyMapper/issues/1989) center the "Trigger and actions" and "Constraint and more" tabs. - [#1392](https://github.com/keymapperorg/KeyMapper/issues/1392) Add action to enable/disable/toggle night shift. -- [#1675](https://github.com/keymapperorg/KeyMapper/issues/1675) Option to make floating buttons movable +- [#1675](https://github.com/keymapperorg/KeyMapper/issues/1675) Option to make floating buttons movable. +- [#1949](https://github.com/keymapperorg/KeyMapper/issues/1949) Floating buttons are completely invisible when pressed if background and border opacity is set to 0. ## [4.0.0 Beta 6](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.06) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt index 2083624e6e..ca911abea6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt @@ -60,13 +60,16 @@ class KeyMapListItemCreator( val triggerKeys = keyMap.trigger.keys.map { key -> when (key) { is AssistantTriggerKey -> assistantTriggerKeyName(key) + is KeyEventTriggerKey -> keyEventTriggerKeyName( key, showDeviceDescriptors, ) is FloatingButtonKey -> floatingButtonKeyName(key) + is FingerprintTriggerKey -> fingerprintKeyName(key) + is EvdevTriggerKey -> evdevTriggerKeyName(key) } } @@ -238,6 +241,13 @@ class KeyMapListItemCreator( if (key.button == null) { append(getString(R.string.deleted_floating_button_text_key_map_list_item)) + } else if (key.button.appearance.text.isBlank()) { + append( + getString( + R.string.floating_button_text_key_map_list_item_empty, + key.button.layoutName, + ), + ) } else { append( getString( @@ -265,7 +275,9 @@ class KeyMapListItemCreator( val deviceName = when (key.device) { is KeyEventTriggerDevice.Internal -> null + is KeyEventTriggerDevice.Any -> getString(R.string.any_device) + is KeyEventTriggerDevice.External -> { if (showDeviceDescriptors) { InputDeviceUtils.appendDeviceDescriptorToName( @@ -349,12 +361,15 @@ class KeyMapListItemCreator( FingerprintGestureType.SWIPE_DOWN -> append( getString(R.string.trigger_key_fingerprint_gesture_down), ) + FingerprintGestureType.SWIPE_UP -> append( getString(R.string.trigger_key_fingerprint_gesture_up), ) + FingerprintGestureType.SWIPE_LEFT -> append( getString(R.string.trigger_key_fingerprint_gesture_left), ) + FingerprintGestureType.SWIPE_RIGHT -> append( getString(R.string.trigger_key_fingerprint_gesture_right), ) 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 51af4cc2fb..8053721ef3 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 @@ -142,10 +142,14 @@ fun TriggerKeyListItem( ) } - is TriggerKeyListItemModel.FloatingButton -> stringResource( - R.string.trigger_key_floating_button_description, - model.buttonName, - ) + 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 diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 9b7f23983e..0a2ddf7718 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1428,19 +1428,19 @@ Configure Use trigger Delete - Button text (Tip: use an emoji) + Button text (optional) (Tip: use an emoji) Button size: Border opacity: Background opacity: Cancel Done - The button must have text! Show over notification panel Show over keyboard Lock in position Floating buttons Floating buttons display over the apps you want. They work just like real buttons, and you can place, style, and map them however you like. Floating button %s (%s) + Floating button (no text) (%s) Deleted floating button Better Caps Lock compatibility Expert Mode provides better compatibility for Caps Lock remapping. Tap \'Use Expert Mode\' and record it again. @@ -1584,6 +1584,7 @@ Side key trigger Fingerprint gesture Floating button: %s + Floating button (no text) Swipe up fingerprint reader Swipe down fingerprint reader Swipe left fingerprint reader From 1ccd872b1ba16302d5de212f7b94bb21e2bd873a Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 20:53:26 +0100 Subject: [PATCH 45/48] fix tests --- .../keymapper/base/backup/BackupManager.kt | 3 + .../keymapper/base/BackupManagerTest.kt | 1 + .../base/keymaps/KeyMapAlgorithmTest.kt | 537 ++++++++++++++---- 3 files changed, 434 insertions(+), 107 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/backup/BackupManager.kt b/base/src/main/java/io/github/sds100/keymapper/base/backup/BackupManager.kt index eebd08fb2d..c79b29a1b9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/backup/BackupManager.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/backup/BackupManager.kt @@ -261,6 +261,9 @@ class BackupManagerImpl @Inject constructor( // Do nothing. Just added columns to floating button entity. JsonMigration(20, 21) { json -> json }, + + // Do nothing. Just added columns to floating button entity. + JsonMigration(21, 22) { json -> json }, ) if (keyMapListJsonArray != null) { diff --git a/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt index 697a8b337a..fe15c2c622 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt @@ -306,6 +306,7 @@ class BackupManagerTest { backgroundOpacity = null, showOverStatusBar = false, showOverInputMethod = false, + isPositionLocked = true, ), ), ) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 52cc168446..c0e5ef5ce6 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -604,7 +604,10 @@ class KeyMapAlgorithmTest { scanCode = Scancode.KEY_B, ) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(1), + ) } @Test @@ -634,7 +637,10 @@ class KeyMapAlgorithmTest { scanCode = Scancode.KEY_B, ) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(1), + ) } @Test @@ -853,7 +859,10 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(trigger.keys[0]) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -1221,8 +1230,14 @@ class KeyMapAlgorithmTest { advanceTimeBy(SEQUENCE_TRIGGER_TIMEOUT) // THEN - verify(performActionsUseCase, never()).perform(copyAction.data) - verify(performActionsUseCase, times(1)).perform(enterAction.data) + verify(performActionsUseCase, never()).perform( + copyAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, times(1)).perform( + enterAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -1273,16 +1288,31 @@ class KeyMapAlgorithmTest { inOrder(performActionsUseCase) { // The single key trigger should not be executed straight away. Wait for // the longer sequence trigger delay. - verify(performActionsUseCase, never()).perform(copyAction.data) + verify(performActionsUseCase, never()).perform( + copyAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // It still shouldn't be executed after the first sequence trigger delay. advanceTimeBy(500) - verify(performActionsUseCase, never()).perform(copyAction.data) + verify(performActionsUseCase, never()).perform( + copyAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) advanceTimeBy(1000) - verify(performActionsUseCase, times(1)).perform(copyAction.data) - verify(performActionsUseCase, never()).perform(sequenceTriggerAction1.data) - verify(performActionsUseCase, never()).perform(sequenceTriggerAction2.data) + verify(performActionsUseCase, times(1)).perform( + copyAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + sequenceTriggerAction1.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + sequenceTriggerAction2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -1314,9 +1344,18 @@ class KeyMapAlgorithmTest { advanceTimeBy(SEQUENCE_TRIGGER_TIMEOUT) // THEN - verify(performActionsUseCase, times(1)).perform(copyAction.data) - verify(performActionsUseCase, never()).perform(pasteAction.data) - verify(performActionsUseCase, never()).perform(enterAction.data) + verify(performActionsUseCase, times(1)).perform( + copyAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + pasteAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + enterAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -1346,9 +1385,18 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(pasteTrigger.keys[0]) // THEN - verify(performActionsUseCase, never()).perform(copyAction.data) - verify(performActionsUseCase, times(1)).perform(pasteAction.data) - verify(performActionsUseCase, never()).perform(enterAction.data) + verify(performActionsUseCase, never()).perform( + copyAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, times(1)).perform( + pasteAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + enterAction.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -1379,9 +1427,18 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(sequenceTrigger.keys[1]) // THEN - verify(performActionsUseCase, never()).perform(copyAction.data) - verify(performActionsUseCase, never()).perform(pasteAction.data) - verify(performActionsUseCase, times(1)).perform(enterAction.data) + verify( + performActionsUseCase, + never(), + ).perform(copyAction.data, device = PerformActionTriggerDevice.AndroidDevice(0)) + verify( + performActionsUseCase, + never(), + ).perform(pasteAction.data, device = PerformActionTriggerDevice.AndroidDevice(0)) + verify( + performActionsUseCase, + times(1), + ).perform(enterAction.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } @Test @@ -1407,11 +1464,19 @@ class KeyMapAlgorithmTest { inOrder(performActionsUseCase) { inputMotionEvent(axisHatX = -1.0f) - verify(performActionsUseCase, times(1)).perform(action.data, InputEventAction.DOWN) + verify(performActionsUseCase, times(1)).perform( + action.data, + InputEventAction.DOWN, + device = PerformActionTriggerDevice.AndroidDevice(1), + ) delay(1000) // Hold down the DPAD button for 1 second. inputMotionEvent(axisHatX = 0.0f) - verify(performActionsUseCase, times(1)).perform(action.data, InputEventAction.UP) + verify(performActionsUseCase, times(1)).perform( + action.data, + InputEventAction.UP, + device = PerformActionTriggerDevice.AndroidDevice(1), + ) } } @@ -1447,7 +1512,10 @@ class KeyMapAlgorithmTest { val consumeUp1 = controller.onMotionEvent(motionEvent) assertThat(consumeUp1, `is`(false)) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(1), + ) } @Test @@ -1477,7 +1545,10 @@ class KeyMapAlgorithmTest { inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(1), + ) } @Test @@ -1504,7 +1575,10 @@ class KeyMapAlgorithmTest { assertThat(consumeUp, `is`(true)) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(1), + ) } @Test @@ -1529,7 +1603,10 @@ class KeyMapAlgorithmTest { assertThat(consumeUp, `is`(true)) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(1), + ) } /** @@ -1576,7 +1653,10 @@ class KeyMapAlgorithmTest { val consumeUp = inputKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.ACTION_UP, repeatCount = 0) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) assertThat(consumeUp, `is`(true)) } @@ -1618,7 +1698,10 @@ class KeyMapAlgorithmTest { assertThat(consumeUp, `is`(true)) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -1660,8 +1743,14 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(shortPressTrigger.keys.first()) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -1703,8 +1792,14 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(shortPressTrigger.keys.first()) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } @Test @@ -1740,8 +1835,14 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(shorterTrigger.keys[0], 600L) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), @@ -1755,8 +1856,14 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(shorterTrigger.keys[0], 1000L) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), @@ -1769,8 +1876,14 @@ class KeyMapAlgorithmTest { // If no triggers are detected mockTriggerKeyInput(shorterTrigger.keys[0], 100L) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + verify(performActionsUseCase, never()).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verify(detectKeyMapsUseCase, times(2)).imitateKeyEvent( any(), any(), @@ -1876,7 +1989,10 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // THEN - verify(performActionsUseCase, times(1)).perform(keyMap1.actionList[0].data) + verify(performActionsUseCase, times(1)).perform( + keyMap1.actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // WHEN assertThat( @@ -1891,8 +2007,14 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // THEN - verify(performActionsUseCase, times(1)).perform(keyMap1.actionList[0].data) - verify(performActionsUseCase, times(1)).perform(keyMap2.actionList[0].data) + verify(performActionsUseCase, times(1)).perform( + keyMap1.actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, times(1)).perform( + keyMap2.actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -1956,8 +2078,14 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // THEN - verify(performActionsUseCase, times(1)).perform(actionList[0].data) - verify(performActionsUseCase, times(1)).perform(actionList[1].data) + verify( + performActionsUseCase, + times(1), + ).perform(actionList[0].data, device = PerformActionTriggerDevice.AndroidDevice(0)) + verify( + performActionsUseCase, + times(1), + ).perform(actionList[1].data, device = PerformActionTriggerDevice.AndroidDevice(0)) } /** @@ -1990,7 +2118,10 @@ class KeyMapAlgorithmTest { // THEN // 3 times because it performs once and then repeats twice - verify(performActionsUseCase, times(3)).perform(action.data) + verify(performActionsUseCase, times(3)).perform( + action.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } @Test @@ -2028,9 +2159,18 @@ class KeyMapAlgorithmTest { // THEN advanceUntilIdle() - verify(performActionsUseCase, times(1)).perform(action1.data) - verify(performActionsUseCase, times(1)).perform(action2.data) - verify(performActionsUseCase, times(1)).perform(action3.data) + verify(performActionsUseCase, times(1)).perform( + action1.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, times(1)).perform( + action2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, times(1)).perform( + action3.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } @Test @@ -2061,8 +2201,14 @@ class KeyMapAlgorithmTest { // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -2091,7 +2237,10 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // THEN - verify(performActionsUseCase, times(action.repeatLimit!! + 1)).perform(action.data) + verify(performActionsUseCase, times(action.repeatLimit!! + 1)).perform( + action.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -2126,7 +2275,10 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(keyMap.trigger.keys[0]) // THEN - verify(performActionsUseCase, times(4)).perform(action.data) + verify(performActionsUseCase, times(4)).perform( + action.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -2160,7 +2312,10 @@ class KeyMapAlgorithmTest { // THEN // performed an extra 2 times each time the trigger is pressed. This is the expected behaviour even for the option to repeat until pressed again. - verify(performActionsUseCase, times(action.repeatLimit!! + 2)).perform(action.data) + verify(performActionsUseCase, times(action.repeatLimit!! + 2)).perform( + action.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -2190,7 +2345,10 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(keyMap.trigger.keys[0], delay = 300) // THEN - verify(performActionsUseCase, times(3)).perform(action.data) + verify( + performActionsUseCase, + times(3), + ).perform(action.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } /** @@ -2220,7 +2378,10 @@ class KeyMapAlgorithmTest { // THEN - verify(performActionsUseCase, times(action.repeatLimit!! + 1)).perform(action.data) + verify(performActionsUseCase, times(action.repeatLimit!! + 1)).perform( + action.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -2270,8 +2431,14 @@ class KeyMapAlgorithmTest { ) // THEN - verify(performActionsUseCase, times(1)).perform(keyMaps[1].actionList[0].data) - verify(performActionsUseCase, never()).perform(keyMaps[0].actionList[0].data) + verify(performActionsUseCase, times(1)).perform( + keyMaps[1].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + keyMaps[0].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // WHEN @@ -2286,8 +2453,14 @@ class KeyMapAlgorithmTest { ) // THEN - verify(performActionsUseCase, times(1)).perform(keyMaps[0].actionList[0].data) - verify(performActionsUseCase, never()).perform(keyMaps[1].actionList[0].data) + verify(performActionsUseCase, times(1)).perform( + keyMaps[0].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + keyMaps[1].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -2340,15 +2513,27 @@ class KeyMapAlgorithmTest { ) // THEN - verify(performActionsUseCase, times(1)).perform(keyMaps[1].actionList[0].data) - verify(performActionsUseCase, never()).perform(keyMaps[0].actionList[0].data) + verify(performActionsUseCase, times(1)).perform( + keyMaps[1].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + keyMaps[0].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // WHEN mockParallelTrigger(keyMaps[0].trigger) // THEN - verify(performActionsUseCase, times(1)).perform(keyMaps[0].actionList[0].data) - verify(performActionsUseCase, never()).perform(keyMaps[1].actionList[0].data) + verify(performActionsUseCase, times(1)).perform( + keyMaps[0].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + keyMaps[1].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -2421,8 +2606,20 @@ class KeyMapAlgorithmTest { ) // THEN - verify(performActionsUseCase, times(1)).perform(keyMaps[0].actionList[0].data) - verify(performActionsUseCase, never()).perform(keyMaps[1].actionList[0].data) + verify( + performActionsUseCase, + times(1), + ).perform( + keyMaps[0].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify( + performActionsUseCase, + never(), + ).perform( + keyMaps[1].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // WHEN inputKeyEvent( @@ -2449,8 +2646,20 @@ class KeyMapAlgorithmTest { ) // THEN - verify(performActionsUseCase, times(1)).perform(keyMaps[1].actionList[0].data) - verify(performActionsUseCase, never()).perform(keyMaps[0].actionList[0].data) + verify( + performActionsUseCase, + times(1), + ).perform( + keyMaps[1].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify( + performActionsUseCase, + never(), + ).perform( + keyMaps[0].actionList[0].data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -2511,7 +2720,10 @@ class KeyMapAlgorithmTest { keyCode = 2, action = KeyEvent.ACTION_UP, ) - verify(performActionsUseCase, never()).perform(action = TEST_ACTION.data) + verify(performActionsUseCase, never()).perform( + action = TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // verify the action is performed and no keys are imitated when triggering the key map // WHEN @@ -2521,7 +2733,10 @@ class KeyMapAlgorithmTest { assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(true)) // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // change the order of the keys being released // WHEN @@ -2531,7 +2746,10 @@ class KeyMapAlgorithmTest { assertThat(inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP), `is`(true)) // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -2612,7 +2830,10 @@ class KeyMapAlgorithmTest { controller.reset() // THEN - verify(performActionsUseCase, times(1)).perform(action.data) + verify(performActionsUseCase, times(1)).perform( + action.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } /** @@ -2654,14 +2875,20 @@ class KeyMapAlgorithmTest { delay(2000) // let it try to repeat // then - verify(performActionsUseCase, times(1)).perform(action1.data) + verify(performActionsUseCase, times(1)).perform( + action1.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verifyNoMoreInteractions() // when long press mockParallelTrigger(trigger2, delay = 2000) // let it repeat // then - verify(performActionsUseCase, atLeast(2)).perform(action2.data) + verify(performActionsUseCase, atLeast(2)).perform( + action2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -2701,14 +2928,20 @@ class KeyMapAlgorithmTest { delay(2000) // let it repeat // then - verify(performActionsUseCase, times(1)).perform(action1.data) + verify( + performActionsUseCase, + times(1), + ).perform(action1.data, device = PerformActionTriggerDevice.AndroidDevice(0)) verifyNoMoreInteractions() // when double press mockTriggerKeyInput(trigger2.keys[0]) // then - verify(performActionsUseCase, times(1)).perform(action2.data) + verify( + performActionsUseCase, + times(1), + ).perform(action2.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } } @@ -2758,20 +2991,29 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // then - verify(performActionsUseCase, times(1)).perform(action1.data) + verify(performActionsUseCase, times(1)).perform( + action1.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verifyNoMoreInteractions() // when long press mockParallelTrigger(trigger2, delay = 2000) // let it repeat // then - verify(performActionsUseCase, atLeast(2)).perform(action2.data) + verify(performActionsUseCase, atLeast(2)).perform( + action2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // when double press mockTriggerKeyInput(trigger3.keys[0]) // then - verify(performActionsUseCase, times(1)).perform(action3.data) + verify(performActionsUseCase, times(1)).perform( + action3.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -2814,14 +3056,20 @@ class KeyMapAlgorithmTest { // then mockParallelTrigger(trigger1) // press the key again to stop it repeating - verify(performActionsUseCase, atLeast(2)).perform(action1.data) + verify(performActionsUseCase, atLeast(2)).perform( + action1.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verifyNoMoreInteractions() // when long press mockParallelTrigger(trigger2, delay = 2000) // let it repeat // then - verify(performActionsUseCase, atLeast(2)).perform(action2.data) + verify(performActionsUseCase, atLeast(2)).perform( + action2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -2866,7 +3114,10 @@ class KeyMapAlgorithmTest { mockParallelTrigger(trigger1) // press the key again to stop it repeating advanceUntilIdle() - verify(performActionsUseCase, atLeast(2)).perform(action1.data) + verify( + performActionsUseCase, + atLeast(2), + ).perform(action1.data, device = PerformActionTriggerDevice.AndroidDevice(0)) verifyNoMoreInteractions() // when double press @@ -2874,7 +3125,10 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // then - verify(performActionsUseCase, times(1)).perform(action2.data) + verify( + performActionsUseCase, + times(1), + ).perform(action2.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } } @@ -2929,14 +3183,20 @@ class KeyMapAlgorithmTest { mockParallelTrigger(trigger1) // press the key again to stop it repeating advanceUntilIdle() - verify(performActionsUseCase, atLeast(2)).perform(action1.data) + verify(performActionsUseCase, atLeast(2)).perform( + action1.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verifyNoMoreInteractions() // when long press mockParallelTrigger(trigger2, delay = 2000) // let it repeat // then - verify(performActionsUseCase, atLeast(2)).perform(action2.data) + verify(performActionsUseCase, atLeast(2)).perform( + action2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // have a delay after a long press of the key is released so a double press isn't detected delay(1000) @@ -2945,7 +3205,10 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(trigger3.keys[0]) // then - verify(performActionsUseCase, times(1)).perform(action3.data) + verify(performActionsUseCase, times(1)).perform( + action3.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verifyNoMoreInteractions() } } @@ -2972,7 +3235,10 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A)) mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS)) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify( + performActionsUseCase, + times(1), + ).perform(TEST_ACTION.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } /** @@ -3041,6 +3307,7 @@ class KeyMapAlgorithmTest { action.data, InputEventAction.DOWN, metaState, + device = PerformActionTriggerDevice.AndroidDevice(123), ) verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( @@ -3055,6 +3322,7 @@ class KeyMapAlgorithmTest { action.data, InputEventAction.UP, 0, + device = PerformActionTriggerDevice.AndroidDevice(123), ) verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( @@ -3110,6 +3378,7 @@ class KeyMapAlgorithmTest { action.data, InputEventAction.DOWN, metaState, + device = PerformActionTriggerDevice.AndroidDevice(123), ) verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( @@ -3132,6 +3401,7 @@ class KeyMapAlgorithmTest { action.data, InputEventAction.UP, 0, + device = PerformActionTriggerDevice.AndroidDevice(123), ) verifyNoMoreInteractions() @@ -3165,8 +3435,14 @@ class KeyMapAlgorithmTest { inputKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.ACTION_UP) inputKeyEvent(KeyEvent.KEYCODE_A, KeyEvent.ACTION_UP) // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // test 2. test triggering 1 key trigger // WHEN @@ -3176,8 +3452,14 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } } @@ -3205,7 +3487,10 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE)) // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } @Test @@ -3255,6 +3540,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform( action.data, InputEventAction.DOWN, + device = PerformActionTriggerDevice.AndroidDevice(0), ) // WHEN @@ -3263,6 +3549,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform( action.data, InputEventAction.UP, + device = PerformActionTriggerDevice.AndroidDevice(0), ) } @@ -3295,23 +3582,23 @@ class KeyMapAlgorithmTest { KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.ACTION_DOWN, metaState = - KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + - KeyEvent.META_SHIFT_ON, + KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, ) inputKeyEvent( KeyEvent.KEYCODE_C, KeyEvent.ACTION_DOWN, metaState = - KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + - KeyEvent.META_SHIFT_ON, + KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, ) inputKeyEvent( KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.ACTION_UP, metaState = - KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + KeyEvent.META_SHIFT_LEFT_ON + - KeyEvent.META_SHIFT_ON, + KeyEvent.META_CTRL_LEFT_ON + KeyEvent.META_CTRL_ON + + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON, ) inputKeyEvent( KeyEvent.KEYCODE_SHIFT_LEFT, @@ -3380,7 +3667,10 @@ class KeyMapAlgorithmTest { ) mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_UP)) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } @Test @@ -3463,7 +3753,10 @@ class KeyMapAlgorithmTest { // then // the first action performed shouldn't be the short press action - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) /* rerun the test to see if the short press trigger action is performed correctly. @@ -3474,7 +3767,10 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // then - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } @Test @@ -3501,7 +3797,10 @@ class KeyMapAlgorithmTest { // THEN // the first action performed shouldn't be the short press action - verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) + verify( + performActionsUseCase, + times(1), + ).perform(TEST_ACTION_2.data, device = PerformActionTriggerDevice.AndroidDevice(0)) // WHEN // rerun the test to see if the short press trigger action is performed correctly. @@ -3509,7 +3808,10 @@ class KeyMapAlgorithmTest { // THEN // the first action performed shouldn't be the short press action - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify( + performActionsUseCase, + times(1), + ).perform(TEST_ACTION.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } @Test @@ -3530,7 +3832,10 @@ class KeyMapAlgorithmTest { TriggerMode.Sequence -> {} } - verify(performActionsUseCase, atLeast(10)).perform(action.data) + verify( + performActionsUseCase, + atLeast(10), + ).perform(action.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } fun params_repeatAction() = listOf( @@ -3742,8 +4047,14 @@ class KeyMapAlgorithmTest { mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) // then - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) + verify(performActionsUseCase, never()).perform( + TEST_ACTION_2.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), @@ -3774,7 +4085,10 @@ class KeyMapAlgorithmTest { advanceUntilIdle() // then - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) // wait for the double press to try and imitate the key. verify(detectKeyMapsUseCase, never()).imitateKeyEvent( @@ -3814,7 +4128,10 @@ class KeyMapAlgorithmTest { any(), any(), ) - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, times(1)).perform( + TEST_ACTION.data, + device = PerformActionTriggerDevice.AndroidDevice(0), + ) } @Test @@ -3864,7 +4181,10 @@ class KeyMapAlgorithmTest { // THEN actionList.forEach { action -> - verify(performActionsUseCase, times(1)).perform(action.data) + verify( + performActionsUseCase, + times(1), + ).perform(action.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } } @@ -4748,7 +5068,10 @@ class KeyMapAlgorithmTest { } // THEN - verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify( + performActionsUseCase, + times(1), + ).perform(TEST_ACTION.data, device = PerformActionTriggerDevice.AndroidDevice(0)) } private suspend fun mockTriggerKeyInput(key: TriggerKey, delay: Long? = null) { From d059f18f7ced4537be6113ce71b23f803d2023f4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 22:08:35 +0100 Subject: [PATCH 46/48] #1949 invisible floating buttons are clickable --- .../keymapper/base/floating/FloatingButtonAppearance.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonAppearance.kt b/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonAppearance.kt index 39031918a4..8c28d0d861 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonAppearance.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonAppearance.kt @@ -16,4 +16,8 @@ data class FloatingButtonAppearance( const val DEFAULT_BACKGROUND_OPACITY = 0.5f const val DEFAULT_BORDER_OPACITY = 1f } + + fun isInvisible(): Boolean { + return text.isBlank() && borderOpacity == 0f && backgroundOpacity == 0f + } } From ed5552a085942e5ba7fc255d43ef86bb7ef088e9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 22:12:54 +0100 Subject: [PATCH 47/48] chore: 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 02a56b5636..e7c52f41de 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ VERSION_NAME=4.0.0-beta.07 -VERSION_CODE=228 +VERSION_CODE=237 From 38cc2c7095a3f2a375f7a9472a91c7f1b215dc7c Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 18 Jan 2026 22:30:45 +0100 Subject: [PATCH 48/48] chore: update whats new --- base/src/main/assets/whats-new.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/base/src/main/assets/whats-new.txt b/base/src/main/assets/whats-new.txt index 4af0dd8f1b..b2e727ee5c 100644 --- a/base/src/main/assets/whats-new.txt +++ b/base/src/main/assets/whats-new.txt @@ -17,6 +17,8 @@ You can now remap ALL buttons when the screen is off (including the power button • Select notification and alarm sounds for Sound action • Constraints for keyboard is showing • Switch next to record trigger button to use PRO mode +• Make floating buttons movable and completely invisible +• Action to toggle night shift ⚙️ Enhanced Controls • Enable or disable all key maps in a group at once @@ -26,6 +28,6 @@ You can now remap ALL buttons when the screen is off (including the power button 🔧 Improvements • Auto-switching keyboard more reliable and quicker on Android 13+ • Wi-Fi connected constraints more reliable -• Various bug fixes and performance optimizations +• A lot of bug fixes -📖 View the complete changelog at: http://changelog.keymapper.club +📖 View the complete changelog at: http://keymapper.app/changelog