Skip to content

Commit

Permalink
Merge branch 'refs/heads/develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
sds100 committed Dec 13, 2024
2 parents a070cad + ced007e commit ba2974a
Show file tree
Hide file tree
Showing 12 changed files with 538 additions and 80 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 8 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> = preferenceRepository
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -549,7 +551,7 @@ class BackupManagerImpl(
val appVersion: Int,

@SerializedName(NAME_KEYMAP_LIST)
val keymapList: List<KeyMapEntity>? = null,
val keyMapList: List<KeyMapEntity>? = null,

@SerializedName(NAME_FINGERPRINT_MAP_LIST)
val fingerprintMapList: List<FingerprintMapEntity>?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<TriggerKeyEntity> = jsonSerializer { (key) ->
when (key) {
is AssistantTriggerKeyEntity -> Gson().toJsonTree(key)
is KeyCodeTriggerKeyEntity -> Gson().toJsonTree(key)
}
}

val DESERIALIZER: JsonDeserializer<TriggerKeyEntity> =
jsonDeserializer { (json, _, _) ->
// nullable because this property was added after backup and restore was released.
Expand Down
4 changes: 2 additions & 2 deletions app/version.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION_NAME=2.7.0
VERSION_CODE=67
VERSION_NAME=2.7.1
VERSION_CODE=68
VERSION_NUM=0
35 changes: 35 additions & 0 deletions docs/contributing/codebase.md
Original file line number Diff line number Diff line change
@@ -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)
28 changes: 1 addition & 27 deletions docs/contributing.md → docs/contributing/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit ba2974a

Please sign in to comment.