diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bb18a10d0..ae4d975367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [2.7.1](https://github.com/sds100/KeyMapper/releases/tag/v2.7.1) + +#### 13 December 2024 + +## 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 + ## [2.7.0](https://github.com/sds100/KeyMapper/releases/tag/v2.7.0) #### 8 December 2024 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a87deef1de..bbf10eaad0 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -64,9 +64,17 @@ -keep class androidx.recyclerview.** { *; } -keep interface androidx.recyclerview.** { *; } +# Keep all the AIDL classes because they must not be ofuscated for the bindings to work. +-keep class android.hardware.input.IInputManager { *; } -keep class android.hardware.input.IInputManager$Stub { *; } +-keep class android.content.pm.IPackageManager { *; } -keep class android.content.pm.IPackageManager$Stub { *; } +-keep class android.permission.IPermissionManager { *; } -keep class android.permission.IPermissionManager$Stub { *; } +-keep class io.github.sds100.keymapper.api.IKeyEventRelayService { *; } +-keep class io.github.sds100.keymapper.api.IKeyEventRelayService$Stub { *; } +-keep class io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback { *; } +-keep class io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback$Stub { *; } -keepattributes *Annotation*, InnerClasses -dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt index cb5271068a..2e54631648 100644 --- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt +++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt @@ -113,10 +113,12 @@ class BackupManagerImpl( .registerTypeAdapter(FingerprintMapEntity.DESERIALIZER) .registerTypeAdapter(KeyMapEntity.DESERIALIZER) .registerTypeAdapter(TriggerEntity.DESERIALIZER) + .registerTypeAdapter(TriggerKeyEntity.SERIALIZER) .registerTypeAdapter(TriggerKeyEntity.DESERIALIZER) .registerTypeAdapter(ActionEntity.DESERIALIZER) .registerTypeAdapter(Extra.DESERIALIZER) - .registerTypeAdapter(ConstraintEntity.DESERIALIZER).create() + .registerTypeAdapter(ConstraintEntity.DESERIALIZER) + .create() } private val backupAutomatically: Flow = preferenceRepository @@ -426,7 +428,7 @@ class BackupManagerImpl( } catch (e: NoSuchElementException) { return Error.CorruptJsonFile(e.message ?: "") } catch (e: Exception) { - e.printStackTrace() + Timber.e(e) if (throwExceptions) { throw e @@ -448,45 +450,45 @@ class BackupManagerImpl( // delete the contents of the file output.clear() - val json = gson.toJson( - BackupModel( - AppDatabase.DATABASE_VERSION, - Constants.VERSION_CODE, - keyMapList, - fingerprintMaps, - defaultLongPressDelay = - preferenceRepository - .get(Keys.defaultLongPressDelay) - .first() - .takeIf { it != PreferenceDefaults.LONG_PRESS_DELAY }, - defaultDoublePressDelay = - preferenceRepository - .get(Keys.defaultDoublePressDelay) - .first() - .takeIf { it != PreferenceDefaults.DOUBLE_PRESS_DELAY }, - defaultRepeatDelay = - preferenceRepository - .get(Keys.defaultRepeatDelay) - .first() - .takeIf { it != PreferenceDefaults.REPEAT_DELAY }, - defaultRepeatRate = - preferenceRepository - .get(Keys.defaultRepeatRate) - .first() - .takeIf { it != PreferenceDefaults.REPEAT_RATE }, - defaultSequenceTriggerTimeout = - preferenceRepository - .get(Keys.defaultSequenceTriggerTimeout) - .first() - .takeIf { it != PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT }, - defaultVibrationDuration = - preferenceRepository - .get(Keys.defaultVibrateDuration) - .first() - .takeIf { it != PreferenceDefaults.VIBRATION_DURATION }, - ), + val backupModel = BackupModel( + AppDatabase.DATABASE_VERSION, + Constants.VERSION_CODE, + keyMapList, + fingerprintMaps, + defaultLongPressDelay = + preferenceRepository + .get(Keys.defaultLongPressDelay) + .first() + .takeIf { it != PreferenceDefaults.LONG_PRESS_DELAY }, + defaultDoublePressDelay = + preferenceRepository + .get(Keys.defaultDoublePressDelay) + .first() + .takeIf { it != PreferenceDefaults.DOUBLE_PRESS_DELAY }, + defaultRepeatDelay = + preferenceRepository + .get(Keys.defaultRepeatDelay) + .first() + .takeIf { it != PreferenceDefaults.REPEAT_DELAY }, + defaultRepeatRate = + preferenceRepository + .get(Keys.defaultRepeatRate) + .first() + .takeIf { it != PreferenceDefaults.REPEAT_RATE }, + defaultSequenceTriggerTimeout = + preferenceRepository + .get(Keys.defaultSequenceTriggerTimeout) + .first() + .takeIf { it != PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT }, + defaultVibrationDuration = + preferenceRepository + .get(Keys.defaultVibrateDuration) + .first() + .takeIf { it != PreferenceDefaults.VIBRATION_DURATION }, ) + val json = gson.toJson(backupModel) + val backupUid = uuidGenerator.random() tempBackupDir = fileAdapter.getPrivateFile("$TEMP_BACKUP_ROOT_DIR/$backupUid") @@ -549,7 +551,7 @@ class BackupManagerImpl( val appVersion: Int, @SerializedName(NAME_KEYMAP_LIST) - val keymapList: List? = null, + val keyMapList: List? = null, @SerializedName(NAME_FINGERPRINT_MAP_LIST) val fingerprintMapList: List?, diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/TriggerTypeConverter.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/TriggerTypeConverter.kt index 392e2d9015..1d34c93091 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/TriggerTypeConverter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/TriggerTypeConverter.kt @@ -18,6 +18,7 @@ class TriggerTypeConverter { fun toTrigger(json: String): TriggerEntity { val gson = GsonBuilder() .registerTypeAdapter(TriggerEntity.DESERIALIZER) + .registerTypeAdapter(TriggerKeyEntity.SERIALIZER) .registerTypeAdapter(TriggerKeyEntity.DESERIALIZER) .registerTypeAdapter(Extra.DESERIALIZER).create() diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt index 0be4e707a2..211cb574b4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt @@ -6,9 +6,12 @@ import com.github.salomonbrys.kotson.byNullableInt import com.github.salomonbrys.kotson.byNullableString import com.github.salomonbrys.kotson.byString import com.github.salomonbrys.kotson.jsonDeserializer +import com.github.salomonbrys.kotson.jsonSerializer import com.github.salomonbrys.kotson.obj +import com.google.gson.Gson import com.google.gson.JsonDeserializer import com.google.gson.JsonElement +import com.google.gson.JsonSerializer import java.util.UUID sealed class TriggerKeyEntity : Parcelable { @@ -26,6 +29,17 @@ sealed class TriggerKeyEntity : Parcelable { const val LONG_PRESS = 1 const val DOUBLE_PRESS = 2 + /** + * This is required because they are subclasses and Gson doesn't know about their + * fields otherwise. + */ + val SERIALIZER: JsonSerializer = jsonSerializer { (key) -> + when (key) { + is AssistantTriggerKeyEntity -> Gson().toJsonTree(key) + is KeyCodeTriggerKeyEntity -> Gson().toJsonTree(key) + } + } + val DESERIALIZER: JsonDeserializer = jsonDeserializer { (json, _, _) -> // nullable because this property was added after backup and restore was released. diff --git a/app/version.properties b/app/version.properties index 1601c40e10..c4b3904667 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=2.7.0 -VERSION_CODE=67 +VERSION_NAME=2.7.1 +VERSION_CODE=68 VERSION_NUM=0 \ No newline at end of file diff --git a/docs/contributing/codebase.md b/docs/contributing/codebase.md new file mode 100644 index 0000000000..e6d624d947 --- /dev/null +++ b/docs/contributing/codebase.md @@ -0,0 +1,35 @@ +# Structure + +This app follows something inspired from Clean Architecture and package-by-feature. + +## Architecture + +All data structures that are persisted are passed around as one of two objects: + +1. **Non-Entity**. This models the data in a way that makes the code more readable and doing the business logic easier. There are no rules for how these need to be named. They should be named what they are. E.g KeyMap, Action, Constraint. + +2. **Entity**. This models how the data should be stored. The class name has an ...Entity suffix. E.g KeyMapEntity. The data is more optimised for storing and the code required to get the data from these models isn't very concise or elegant. The developer took some strange decisions in the first versions of this app. ๐Ÿ˜† + +Every screen in the app has a view model and the view model interacts with one or multiple *use cases* (more below). The view model converts data that needs to be shown to the user into something that can populate the user interface. For example, the data values in the Action object isn't very useful to the user so this needs to be converted into strings and images that do mean something to the user. All the view models have a ResourceProvider dependency which is how they get strings, Drawables and colours from the resources without having to use a Context. This isn't a problem for configuration changes (e.g locale change) because the activity is recreated, which means all the resources are re-fetched in the view model. + +The use cases contains all the business logic in the app. A *use case* interacts with the adapters and repositories mentioned below. A use case is made for everything that can be done in the app. E.g configuring a key map, displaying a mapping, configuring settings, onboarding the user. Most use cases correspond to something that *the user can do* in the app but some do not because they contain complicated code that is used in multiple use cases. E.g the GetActionErrorUseCase which determines if an action can be performed successfully. + +Adapters and repositories contain all interactions with the Android framework (except UI stuff). This is so that tests can be more easily written and executed. Android often changes what apps are allowed to do and how so abstracting these interactions away means the code only needs to be changed in a single place. This means that the only place that a Context object is used is in Services, Activities, Fragments and the adapters. + +## Package by feature + +Every package contains files related to each other. For example, everything (view models, fragments, use cases) to do with constraints is stored in one package. +The only package which isn't a feature is the `data` package because it is useful to have some of the things in there together, e.g the migrations. +The `system` package bundles all the packages which are related to the Android framework because there are so many. + +![contributing-app-structure](../images/contributing-app-structure.png) + +# Key event detection and input + +The diagram below shows how key events are passed around Key Mapper on Android 14+. This change was required because in Android 14 Android restricted the rate at which intents can be broadcast to once per second when an app is backgrounded. This is too slow for repeating key event actions in Key Mapper. Key Mapper still uses broadcast receivers to send key events between the accessibility service and input method on older Android versions to reduce the chance of breaking everyone's key maps. As shown in the diagram this is a bit complicated and potentially over-engineered but it must be two-way and future proof to any further restrictions. Using manifest-defined broadcast receivers, that aren't rate limited isn't an elegant solution because one has to pass messages between these broadcast receivers and the services through some 3rd class. Binder is lower latency than using intents and is synchronous whereas broadcast receivers are asynchronous. Apps are not allowed to bind to accessibility services so a new "relay" service needed to be made to link the accessibility and input method services. + +The accessibility service is where triggers are detected by listening to the key events that Android system sends it. Key event and text actions send their key events from the accessibility service to the relay service, which then forwards it to the input method. + +The code for input methods to talk to the Key Event relay service can be found in KeyEventRelayServiceWrapper. Key events are listened to from the input method service when Android blocks key events to the accessibility service during phone calls. + +![key-event-relay-service](../images/key-event-relay-service.svg) \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing/introduction.md similarity index 77% rename from docs/contributing.md rename to docs/contributing/introduction.md index 37e44f496a..a18b01afd0 100644 --- a/docs/contributing.md +++ b/docs/contributing/introduction.md @@ -18,7 +18,7 @@ You can get the apks for the pre-release versions in 2 ways: ### How can I help? - Test and experiment new features. All features and bug-fixes that are being worked on for a release can be found on the Projects page [here](https://github.com/keymapperorg/KeyMapper/projects). -- If you find any bugs or crashes then report them by following the guide [here](report-issues.md). +- If you find any bugs or crashes then report them by following the guide [here](../report-issues.md). ## Contributing code @@ -46,32 +46,6 @@ There are also 4 build types, which have different optimizations and package nam - **debug_release** = This is a debug build type that does not include a package name suffix so that it is possible to test how the production app will look. It is the only way to get the Google Play Billing library functioning because it will break if the package name isn't the same as on the Play store. - **ci** = This is used for alpha builds to the community in Discord. It includes some optimizations to ensure it is performant but doesn't obfuscate the code so it is possible to understand logs and bug reports. It has a `.ci` package name suffix. -### Introduction to the structure - -This app follows something inspired from Clean Architecture and package-by-feature. - -#### Architecture - -All data structures that are persisted are passed around as one of two objects: - -1. **Non-Entity**. This models the data in a way that makes the code more readable and doing the business logic easier. There are no rules for how these need to be named. They should be named what they are. E.g KeyMap, Action, Constraint. - -2. **Entity**. This models how the data should be stored. The class name has an ...Entity suffix. E.g KeyMapEntity. The data is more optimised for storing and the code required to get the data from these models isn't very concise or elegant. The developer took some strange decisions in the first versions of this app. ๐Ÿ˜† - -Every screen in the app has a view model and the view model interacts with one or multiple *use cases* (more below). The view model converts data that needs to be shown to the user into something that can populate the user interface. For example, the data values in the Action object isn't very useful to the user so this needs to be converted into strings and images that do mean something to the user. All the view models have a ResourceProvider dependency which is how they get strings, Drawables and colours from the resources without having to use a Context. This isn't a problem for configuration changes (e.g locale change) because the activity is recreated, which means all the resources are re-fetched in the view model. - -The use cases contains all the business logic in the app. A *use case* interacts with the adapters and repositories mentioned below. A use case is made for everything that can be done in the app. E.g configuring a key map, displaying a mapping, configuring settings, onboarding the user. Most use cases correspond to something that *the user can do* in the app but some do not because they contain complicated code that is used in multiple use cases. E.g the GetActionErrorUseCase which determines if an action can be performed successfully. - -Adapters and repositories contain all interactions with the Android framework (except UI stuff). This is so that tests can be more easily written and executed. Android often changes what apps are allowed to do and how so abstracting these interactions away means the code only needs to be changed in a single place. This means that the only place that a Context object is used is in Services, Activities, Fragments and the adapters. - -#### Package by feature - -Every package contains files related to each other. For example, everything (view models, fragments, use cases) to do with constraints is stored in one package. -The only package which isn't a feature is the `data` package because it is useful to have some of the things in there together, e.g the migrations. -The `system` package bundles all the packages which are related to the Android framework because there are so many. - -![contributing-app-structure](images/contributing-app-structure.png) - ### Branches ๐ŸŒด - master: Everything in the latest stable release. diff --git a/docs/images/key-event-relay-service.svg b/docs/images/key-event-relay-service.svg new file mode 100644 index 0000000000..b6ce50c070 --- /dev/null +++ b/docs/images/key-event-relay-service.svg @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Key Event Relay Service +
+
+
+
+ Key Event Relay Serv... + +
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ Input Method Service +
+
+
+
+ Input Method Service + +
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ Callback +
+
+
+
+ Callback + +
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ Accessibility Service +
+
+
+
+ Accessibility Service + +
+
+
+
+ + + + + + + + +
+
+
+ Key Mapper +
+
+
+
+ Key Mapper + +
+
+
+
+ + + + + +
+
+
+ + The arrows mark the flow of key events + +
+
+
+
+ The arrows mark the flow of key events + +
+
+
+
+ + + + + + + + +
+
+
+ The keyboard binds to the relay service insideย  + KeyEventRelayServiceWrapperImpl +
+
+
+
+ The keyboard binds to the... + +
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ Callback +
+
+
+
+ Callback + +
+
+
+
+ + + + + + + + +
+
+
+ The relay service forwards key events from the Input + Method service to the accessibility service's + callback. +
+
+
+
+ The relay service forwa... + +
+
+
+
+ + + + + + + + +
+
+
+ A callback is registered so the service can receive + key events. +
+
+
+
+ A callback is registere... + +
+
+
+
+ + + + + + + + +
+
+
+ The accessibility service sends key events to the + input method service by calling sendKeyEventย in + the relay service. +
+
+
+
+ The accessibility servi... + +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/docs/user-guide/keymaps.md b/docs/user-guide/keymaps.md index 548850ebe7..cbb140082d 100644 --- a/docs/user-guide/keymaps.md +++ b/docs/user-guide/keymaps.md @@ -77,9 +77,9 @@ This trigger allows you to remap the various ways that your devices trigger the There are 3 assistant options you can choose: -- **Device assistant** = This is the assistant usually triggered from a long press of a power button or a dedicated button. -- **Voice assistant** = This is the assistant launched from the hands-free voice button on keyboards/headsets. -- **Any assistant** = This will trigger the key map when any of the above are triggered. +- **Device assistant**: This is the assistant usually triggered from a long press of a power button or a dedicated button. +- **Voice assistant**: This is the assistant launched from the hands-free voice button on keyboards/headsets. +- **Any assistant**: This will trigger the key map when any of the above are triggered. !!! note It is not possible to create long-press key maps with this trigger! But you can do double press. You also can not use multiple assistant triggers in a parallel trigger because there is no way to detect them being pressed at exactly the same time. @@ -103,10 +103,10 @@ This *should* work on Samsung devices that have a dedicated Bixby button but als **Voice assistant button on keyboards and Bluetooth headphones** -Many external devices such as headsets, keyboards have a button for launching the voice assistant so you can control your phone hands-free. This also works with Key Mapper. If you +Many external devices such as headsets and keyboards have a button for launching the voice assistant so you can control your phone hands-free. This also works with Key Mapper. !!! warning - Some headphones have hardcoded the assistant apps that they will launch and will not work with Key Mapper. The developer has Sony WH1000XM3 headphones that support either Alexa or Google Assistant and refuse to launch Key Mapper when it is selected as the default assistant app. + Some headphones have hardcoded the assistant apps that they support and will not work with Key Mapper. The developer has Sony WH1000XM3 headphones that only support Alexa and Google Assistant and refuse to launch Key Mapper when it is selected as the default assistant app. ## Customising actions diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 52259dcc03..7ee589ae36 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -27,8 +27,9 @@ lane :prod do version_code = get_properties_value(key: "VERSION_CODE", path: "./app/version.properties") version_name = get_properties_value(key: "VERSION_NAME", path: "./app/version.properties") - whats_new = File.read("../app/src/main/assets/whats-new.txt") - File.write("metadata/android/en-US/changelogs/" + version_code + ".txt", whats_new) +# Don't create changelog for f-droid because not committing it +# whats_new = File.read("../app/src/main/assets/whats-new.txt") +# File.write("metadata/android/en-US/changelogs/" + version_code + ".txt", whats_new) gradle(task: "testDebugUnitTest") @@ -49,10 +50,12 @@ lane :prod do # Do not release a debug build for pro version. # gradle(task: "assembleDebug") - gradle(task: "assembleProRelease") + +# Release the free build to GitHub because billing only works if signed by Google Play + gradle(task: "assembleFreeRelease") gradle(task: "bundleProRelease") - apk_path_release="app/build/outputs/apk/pro/release/keymapper-" + version_name + ".apk" + apk_path_release="app/build/outputs/apk/free/release/keymapper-" + version_name + ".apk" github_release = set_github_release( repository_name: "keymapperorg/KeyMapper", diff --git a/mkdocs.yml b/mkdocs.yml index d81bdb28c4..380b62cb26 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,7 @@ nav: - Home: index.md - Quick Start Guide: quick-start.md - FAQ: faq.md - - Share Your Key Maps Here! ๐Ÿ‘: sharing.md + # - Share Your Key Maps Here! ๐Ÿ‘: sharing.md - User Guide: - Key Maps: user-guide/keymaps.md - Fingerprint Gesture Maps (2.2.0+, Android 8.0+): user-guide/fingerprint-gestures.md @@ -22,7 +22,9 @@ nav: - Installing on Oculus Quest: user-guide/oculus.md - Shizuku support (2.4.0+, Android 6.0+): user-guide/shizuku.md - API: user-guide/api.md - - Contributing: contributing.md + - Contributing: + - Introduction: contributing/introduction.md + - Code base: contributing/codebase.md - Report Issues: report-issues.md - Known Issues: known-issues.md