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:
-
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.
-
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.
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.