diff --git a/CHANGELOG.md b/CHANGELOG.md index db78ba37e9..f3c63b86ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,44 @@ +## [4.0.0 Beta 7](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.07) + +#### TO BE RELEASED + +## Added + +- [#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](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](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. +- [#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+. +- [#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. +- [#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) #### 4 January 2026 ## 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 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. +- [#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](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) @@ -19,12 +47,13 @@ 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) @@ -37,21 +66,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) @@ -60,18 +89,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) @@ -80,7 +109,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 @@ -89,7 +118,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) @@ -97,22 +126,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 @@ -130,16 +159,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) @@ -155,33 +184,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) @@ -190,13 +219,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) @@ -205,10 +234,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 @@ -216,11 +245,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) @@ -228,31 +257,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) @@ -260,7 +289,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) @@ -268,11 +297,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) @@ -282,8 +311,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 @@ -292,7 +321,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) @@ -305,10 +334,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 @@ -322,8 +351,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 @@ -334,21 +363,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) @@ -360,26 +389,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) @@ -388,12 +417,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) @@ -401,13 +430,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) @@ -415,14 +444,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) @@ -430,30 +459,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) @@ -461,15 +490,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) @@ -477,9 +506,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) @@ -487,21 +516,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) @@ -511,29 +540,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) @@ -906,63 +935,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. @@ -977,7 +1006,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 @@ -1001,8 +1030,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. @@ -1013,21 +1042,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) @@ -1035,14 +1064,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) @@ -1055,80 +1084,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) @@ -1138,26 +1167,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 @@ -1166,7 +1195,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 @@ -1175,15 +1204,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. @@ -1191,15 +1220,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. @@ -1211,15 +1240,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 @@ -1236,9 +1265,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. @@ -1246,15 +1275,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. @@ -1266,35 +1295,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) @@ -1707,7 +1736,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) 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! 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/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/app/version.properties b/app/version.properties index c4fdc8c42a..e7c52f41de 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=237 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/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 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..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 @@ -30,6 +30,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 +210,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/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 9bd390fccc..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 @@ -1060,7 +1083,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/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/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/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 77171ed65a..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( @@ -375,7 +381,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) } @@ -459,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 -> { @@ -1039,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/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/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/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..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() @@ -910,10 +909,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 +1077,6 @@ class KeyMapAlgorithm( key.matchesEvent(event.withShortPress) -> true key.matchesEvent(event.withLongPress) -> true key.matchesEvent(event.withDoublePress) -> true - else -> false } @@ -1794,6 +1797,7 @@ class KeyMapAlgorithm( return when (this.device) { KeyEventTriggerDevice.Any -> codeMatches && this.clickType == event.clickType + is KeyEventTriggerDevice.External -> event.isExternal && codeMatches && @@ -1990,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/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/expertmode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt index cb69b61f41..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 @@ -69,54 +69,76 @@ 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 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() + } + } + // 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 -> + when { + !isWifiConnected -> { + AutoStartEligibility.NotEligible.WiFiDisconnected + } + + !isWriteSecureSettingsGranted -> { + AutoStartEligibility.NotEligible.WriteSecureSettingsRevoked + } + + !setupController.isAdbPaired() -> { + AutoStartEligibility.NotEligible.AdbUnpaired + } + + else -> { + AutoStartEligibility.Eligible(AutoStartType.ADB) + } + } } + } else { + flowOf(AutoStartEligibility.NotEligible.ShizukuRootRequired) } } } @@ -125,30 +147,9 @@ 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 || - connectionState.isStoppedByUser || - !getIsUsedBefore() || - getIsStoppedByUser() || - isSystemBridgeEmergencyKilled() || - !isAutoStartEnabled() - ) { - flowOf(null) - } 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) - } else { - autoStartTypeFlow - } + getAutoStartEligibility(connectionState) } /** @@ -158,7 +159,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()) { @@ -178,10 +179,57 @@ class SystemBridgeAutoStarter @Inject constructor( autoStartFlow .distinctUntilChanged() // Must come before the filterNotNull - .filterNotNull() - .collectLatest { type -> - autoStart(type) - } + .collectLatest(::processAutoStartEligibility) + } + } + + 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) + + 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() + + else -> { + Timber.w("Not auto starting the system bridge: $eligibility") + } } } @@ -230,7 +278,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 +346,22 @@ class SystemBridgeAutoStarter @Inject constructor( */ private suspend fun isWithinAutoStartCooldown(): Boolean { val lastAutoStartTime = preferences.get(Keys.systemBridgeLastAutoStartTime).first() - return lastAutoStartTime != null && - clock.elapsedRealtime() - lastAutoStartTime < (5 * 60_000) + val lastManualStartTime = preferences.get(Keys.systemBridgeLastManualStartTime).first() + val currentTime = clock.unixTimestamp() + + 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) } private suspend fun isAutoStartEnabled(): Boolean { @@ -336,7 +401,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) @@ -354,7 +439,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/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt index 73f14b3120..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 @@ -315,18 +315,41 @@ 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. + nonInteractivePairingCodeNotification() - interactionStep = InteractionStep.PAIR_DEVICE + interactionStep = null + } } - else -> return // Do not start interaction timeout job + else -> return } + } - startInteractionTimeoutJob() + 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() { 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..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 @@ -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 @@ -22,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) @@ -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 { @@ -56,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() { @@ -106,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, @@ -192,27 +208,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 { @@ -342,3 +361,9 @@ interface SystemBridgeSetupUseCase { suspend fun getShellStartCommand(): KMResult } + +enum class AdbAutoStartEligibility { + ELIGIBLE, + CHECKING, + NOT_ELIGIBLE, +} 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 + } } 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/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/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/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( 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/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 } 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..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 @@ -1,10 +1,14 @@ 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.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 +34,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 +47,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 { @@ -95,7 +102,7 @@ class ConfigSettingsUseCaseImpl @Inject constructor( get() = devicesAdapter.connectedInputDevices override suspend fun enableCompatibleIme() { - imeHelper.enableCompatibleInputMethods() + imeHelper.enableCompatibleInputMethod() } override suspend fun chooseCompatibleIme(): KMResult = @@ -194,6 +201,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 +260,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..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 @@ -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/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/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/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) } 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/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() { 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") } 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/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..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 @@ -2,7 +2,10 @@ 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.foundation.text.TextAutoSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -13,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( @@ -25,24 +29,20 @@ 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, + .padding(horizontal = 8.dp, vertical = 4.dp), + 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), + autoSize = TextAutoSize.StepBased( + minFontSize = 10.sp, + maxFontSize = MaterialTheme.typography.bodyLarge.fontSize, + ), text = text, style = if (isEnabled) { MaterialTheme.typography.bodyLarge @@ -54,7 +54,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, ) } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index dceb055592..0a2ddf7718 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 @@ -692,6 +692,11 @@ Light Dark System + + Language + Choose the app language + System default + @@ -824,7 +829,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 +838,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! @@ -956,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 @@ -1379,7 +1387,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 +1402,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. @@ -1420,18 +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. @@ -1575,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 @@ -1752,6 +1762,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. @@ -1776,6 +1789,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/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/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) { 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..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 @@ -27,6 +31,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 +143,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) @@ -447,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 @@ -505,10 +554,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() @@ -521,7 +577,7 @@ class SystemBridgeAutoStarterTest { // It is killed unexpectedly straight after auto starting connectionStateFlow.value = SystemBridgeConnectionState.Disconnected( time = 7000, - isStoppedByUser = true, + isStoppedByUser = false, ) advanceUntilIdle() @@ -530,6 +586,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/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..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 @@ -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( @@ -527,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 @@ -557,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 @@ -776,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), + ) } /** @@ -1144,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), + ) } /** @@ -1196,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), + ) } } @@ -1237,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), + ) } /** @@ -1269,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), + ) } /** @@ -1302,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 @@ -1330,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), + ) } } @@ -1370,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 @@ -1400,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 @@ -1427,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 @@ -1452,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), + ) } /** @@ -1499,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)) } @@ -1541,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), + ) } /** @@ -1583,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), + ) } /** @@ -1626,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 @@ -1663,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(), @@ -1678,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(), @@ -1692,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(), @@ -1799,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( @@ -1814,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), + ) } } @@ -1832,16 +2031,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 - } + 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) - } - }) + override fun getErrors(actions: List): Map { + return mapOf(actionList[0].data to KMError.NoCompatibleImeChosen) + } + }, + ) assertThat( inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN), @@ -1877,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)) } /** @@ -1911,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 @@ -1949,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 @@ -1982,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), + ) } /** @@ -2012,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), + ) } /** @@ -2047,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), + ) } /** @@ -2081,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), + ) } /** @@ -2111,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)) } /** @@ -2141,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), + ) } /** @@ -2191,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 @@ -2207,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), + ) } } @@ -2261,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), + ) } } @@ -2342,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( @@ -2370,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), + ) } } @@ -2432,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 @@ -2442,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 @@ -2452,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), + ) } } @@ -2533,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), + ) } /** @@ -2575,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), + ) } } @@ -2622,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)) } } @@ -2679,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), + ) } } @@ -2735,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), + ) } } @@ -2787,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 @@ -2795,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)) } } @@ -2850,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) @@ -2866,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() } } @@ -2893,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)) } /** @@ -2962,6 +3307,7 @@ class KeyMapAlgorithmTest { action.data, InputEventAction.DOWN, metaState, + device = PerformActionTriggerDevice.AndroidDevice(123), ) verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( @@ -2976,6 +3322,7 @@ class KeyMapAlgorithmTest { action.data, InputEventAction.UP, 0, + device = PerformActionTriggerDevice.AndroidDevice(123), ) verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( @@ -3031,6 +3378,7 @@ class KeyMapAlgorithmTest { action.data, InputEventAction.DOWN, metaState, + device = PerformActionTriggerDevice.AndroidDevice(123), ) verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( @@ -3053,6 +3401,7 @@ class KeyMapAlgorithmTest { action.data, InputEventAction.UP, 0, + device = PerformActionTriggerDevice.AndroidDevice(123), ) verifyNoMoreInteractions() @@ -3086,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 @@ -3097,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), + ) } } @@ -3126,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 @@ -3176,6 +3540,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform( action.data, InputEventAction.DOWN, + device = PerformActionTriggerDevice.AndroidDevice(0), ) // WHEN @@ -3184,6 +3549,7 @@ class KeyMapAlgorithmTest { verify(performActionsUseCase, times(1)).perform( action.data, InputEventAction.UP, + device = PerformActionTriggerDevice.AndroidDevice(0), ) } @@ -3216,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, @@ -3301,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 @@ -3384,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. @@ -3395,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 @@ -3422,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. @@ -3430,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 @@ -3451,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( @@ -3663,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(), @@ -3695,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( @@ -3735,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 @@ -3785,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)) } } @@ -4669,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) { 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/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/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/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index a51d8d1e9d..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,10 +145,8 @@ 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 systemBridgeLastManualStartTime = + longPreferencesKey("key_system_bridge_last_manual_start_time") val systemBridgeLastAutoStartTime = longPreferencesKey("key_system_bridge_last_auto_start_time") val keyEventActionsUseSystemBridge = 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 diff --git a/evdev/src/main/rust/evdev/.cargo/config.toml b/evdev/src/main/rust/evdev/.cargo/config.toml index f2084c6b7f..48e2761303 100644 --- a/evdev/src/main/rust/evdev/.cargo/config.toml +++ b/evdev/src/main/rust/evdev/.cargo/config.toml @@ -13,3 +13,5 @@ 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..48e2761303 100644 --- a/evdev/src/main/rust/evdev_manager/.cargo/config.toml +++ b/evdev/src/main/rust/evdev_manager/.cargo/config.toml @@ -13,3 +13,5 @@ rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] + + 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 848b0d0f9c..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()); @@ -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; 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 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" 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 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 } 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) 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..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 @@ -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/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) } } 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", ) } 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!!) } /** 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( 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") 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 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..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 @@ -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,15 +67,35 @@ 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 } } } } } + /** + * 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()) @@ -83,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( @@ -105,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 @@ -303,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