diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index b08635993b..3caac0256d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -60,6 +60,15 @@ body:
validations:
required: true
+ - type: input
+ id: android_version
+ attributes:
+ label: Android version
+ description: Go to your Settings app and find your _Android_ version. NOT the HyperOS, MIUI, OxygenOS, OneUI version.
+ placeholder: Android 15
+ validations:
+ required: true
+
- type: input
id: device
attributes:
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index a2027703b1..1acb88303f 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -17,9 +17,6 @@ jobs:
java-version: 17
cache: 'gradle'
- - name: Setup Android SDK
- uses: android-actions/setup-android@v2
-
- name: Unit tests
run: bash ./gradlew testDebugUnitTest
@@ -46,12 +43,48 @@ jobs:
java-version: 17
cache: 'gradle'
- - name: Setup Android SDK
- uses: android-actions/setup-android@v2
-
- name: Ktlint check
run: ./gradlew ktlintCheck
+ rust:
+ name: Rust code style and tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt, clippy
+
+ - name: Install Android Rust targets
+ run: |
+ rustup target add armv7-linux-androideabi
+ rustup target add aarch64-linux-android
+ rustup target add i686-linux-android
+ rustup target add x86_64-linux-android
+
+ - uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/bin/
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ evdev/src/main/rust/evdev_manager/target/
+ key: ${{ runner.os }}-rust-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-rust-
+
+ - name: Check Rust formatting
+ working-directory: evdev/src/main/rust/evdev_manager
+ run: cargo fmt --check
+
+ - name: Run Rust tests
+ working-directory: evdev/src/main/rust/evdev_manager
+ run: cargo test --package evdev_manager_core
+
apk:
name: Build APK
runs-on: ubuntu-latest
@@ -77,6 +110,18 @@ jobs:
- name: Setup Android SDK
uses: android-actions/setup-android@v2
+ with:
+ ndk-version: "27.2.12479018"
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Install Android Rust targets
+ run: |
+ rustup target add armv7-linux-androideabi
+ rustup target add aarch64-linux-android
+ rustup target add i686-linux-android
+ rustup target add x86_64-linux-android
- name: set up Ruby for fastlane
uses: ruby/setup-ruby@v1
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 18188155de..5fc9e345b2 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -23,9 +23,6 @@ jobs:
java-version: 17
cache: 'gradle'
- - name: Setup Android SDK
- uses: android-actions/setup-android@v2
-
- name: Unit tests
run: bash ./gradlew testDebugUnitTest
@@ -52,14 +49,50 @@ jobs:
java-version: 17
cache: 'gradle'
- - name: Setup Android SDK
- uses: android-actions/setup-android@v2
-
- name: Ktlint check
run: ./gradlew ktlintCheck
+ rust:
+ name: Rust code style and tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt, clippy
+
+ - name: Install Android Rust targets
+ run: |
+ rustup target add armv7-linux-androideabi
+ rustup target add aarch64-linux-android
+ rustup target add i686-linux-android
+ rustup target add x86_64-linux-android
+
+ - uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/bin/
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ evdev/src/main/rust/evdev_manager/target/
+ key: ${{ runner.os }}-rust-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-rust-
+
+ - name: Check Rust formatting
+ working-directory: evdev/src/main/rust/evdev_manager
+ run: cargo fmt --check
+
+ - name: Run Rust tests
+ working-directory: evdev/src/main/rust/evdev_manager
+ run: cargo test --package evdev_manager_core
+
apk:
- name: Generate and upload APK to Discord
+ name: Build APK
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -89,6 +122,18 @@ jobs:
- name: Setup Android SDK
uses: android-actions/setup-android@v2
+ with:
+ ndk-version: "27.2.12479018"
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Install Android Rust targets
+ run: |
+ rustup target add armv7-linux-androideabi
+ rustup target add aarch64-linux-android
+ rustup target add i686-linux-android
+ rustup target add x86_64-linux-android
- name: set up Ruby for fastlane
uses: ruby/setup-ruby@v1
@@ -122,21 +167,6 @@ jobs:
name: ${{ env.APK_NAME }}
path: app/build/outputs/apk/ci/${{ env.APK_NAME }}.apk
- - name: Upload to Discord
- uses: sinshutu/upload-to-discord@v2.0.0
- if: github.event.repository.fork == false
- env:
- DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- with:
- args: app/build/outputs/apk/ci/${{ env.APK_NAME }}.apk
-
- - name: Report build status to Discord
- uses: sarisia/actions-status-discord@v1
- if: github.event.repository.fork == false && failure()
- with:
- title: "Build apk"
- webhook: ${{ secrets.DISCORD_BUILD_STATUS_WEBHOOK }}
-
synchronize-with-crowdin:
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index e0116b2eb1..9d7b825226 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,3 +94,7 @@ app/.env
/.idea/AndroidProjectSystem.xml
/.idea/runConfigurations.xml
/.idea/studiobot.xml
+
+evdev/build
+evdev/.cxx
+evdev/src/main/rust/*/target
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e66269720..be7db7909c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,19 +1,49 @@
+## [4.0.0 Beta 4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.04)
+
+#### 25 December 2025
+
+Merry Christmas from the Key Mapper team! 🎄
+
+Renamed PRO mode to Expert mode because it sounded like a paid premium feature even though it is 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.
+
+## 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
+ device.
+- #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)
#### 25 November 2025
## 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.
## 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 they introduced.
+- #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)
@@ -21,12 +51,15 @@
## Added
-- #1890 add button to save log to file and share it. The clipboard button now cuts off older entries and keeps newest ones.
+- #1890 add button to save log to file and share it. The clipboard button now cuts off older entries
+ and keeps newest ones.
## Fixed
-- Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to 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.
+- Only autostart PRO mode with Shizuku if Shizuku permission is granted. Otherwise fallback to
+ 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.
## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01)
diff --git a/README.md b/README.md
index 854cd7cd51..473668047b 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2b0504c7a9..7d234f10e3 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -152,6 +152,7 @@ dependencies {
implementation(project(":data"))
implementation(project(":sysbridge"))
implementation(project(":system"))
+ implementation(project(":evdev"))
compileOnly(project(":systemstubs"))
coreLibraryDesugaring(libs.desugar.jdk.libs)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 3ba9020698..1e905036f2 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -116,7 +116,7 @@
# Keep all AIDL interface classes and their methods
-keep class io.github.sds100.keymapper.sysbridge.ISystemBridge** { *; }
--keep class io.github.sds100.keymapper.sysbridge.IEvdevCallback** { *; }
+-keep class io.github.sds100.keymapper.evdev.IEvdevCallback** { *; }
-keep class io.github.sds100.keymapper.sysbridge.IShizukuStarterService** { *; }
-keepclassmembers class io.github.sds100.keymapper.sysbridge.shizuku.ShizukuStarterService {
@@ -138,7 +138,10 @@
-keep class io.github.sds100.keymapper.sysbridge.** extends android.content.ContentProvider { *; }
# Keep parcelable classes used in AIDL
--keep class io.github.sds100.keymapper.common.models.EvdevDeviceHandle { *; }
+-keep class io.github.sds100.keymapper.common.models.GrabbedDeviceHandle { *; }
+-keep class io.github.sds100.keymapper.common.models.EvdevDeviceInfo { *; }
+-keep class io.github.sds100.keymapper.common.models.GrabTargetKeyCode { *; }
+-keep class io.github.sds100.keymapper.common.models.ShellResult { *; }
# Keep all rikka.hidden classes and interfaces as they contain AIDL files
-keep class rikka.hidden.** { *; }
@@ -241,4 +244,4 @@
-dontwarn android.view.DisplayInfo
-dontwarn android.view.IWindowManager**
-dontwarn com.android.internal.app.**
--dontwarn com.android.internal.policy.**
\ No newline at end of file
+-dontwarn com.android.internal.policy.**
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c4e87ba265..d74d55e0eb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,7 +13,7 @@
-
+ viewModelScope.launch {
- navigateToAdvancedTriggers("purchase_assistant_trigger")
- }
+ TriggerSetupShortcut.ASSISTANT,
+ TriggerSetupShortcut.FLOATING_BUTTON_CUSTOM,
+ TriggerSetupShortcut.FLOATING_BUTTON_LOCK_SCREEN,
+ -> viewModelScope.launch {
+ navigateToAdvancedTriggers("purchase_assistant_trigger")
+ }
- else -> super.showTriggerSetup(shortcut, forceProMode)
+ else -> super.showTriggerSetup(shortcut, forceExpertMode)
}
}
}
diff --git a/app/version.properties b/app/version.properties
index 003ec7e3c3..6a17534a00 100644
--- a/app/version.properties
+++ b/app/version.properties
@@ -1,3 +1,2 @@
-VERSION_NAME=4.0.0-beta.3
-VERSION_CODE=194
-VERSION_NUM=01
\ No newline at end of file
+VERSION_NAME=4.0.0-beta.04
+VERSION_CODE=217
diff --git a/base/src/main/assets/whats-new.txt b/base/src/main/assets/whats-new.txt
index 4a199f4438..4af0dd8f1b 100644
--- a/base/src/main/assets/whats-new.txt
+++ b/base/src/main/assets/whats-new.txt
@@ -1,5 +1,5 @@
✨ Screen-off remapping
-You can now remap ALL buttons when the screen is off (including the power button) for free with PRO mode.
+You can now remap ALL buttons when the screen is off (including the power button) for free with Expert mode.
🎯 New Actions
• Run shell commands
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt
index ac3706a965..78ba5f4ad1 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt
@@ -29,9 +29,9 @@ class ActivityViewModel @Inject constructor(
setupAccessibilityServiceDelegate.showCantFindAccessibilitySettingsDialog()
}
- fun launchProModeSetup() {
+ fun launchExpertModeSetup() {
viewModelScope.launch {
- navigate("pro_mode_setup", NavDestination.ProModeSetup)
+ navigate("expert_mode_setup", NavDestination.ExpertModeSetup)
}
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt
index 0ea1d4b5c2..e989b9748c 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt
@@ -16,8 +16,9 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
+import io.github.sds100.keymapper.base.expertmode.SystemBridgeAutoStarter
import io.github.sds100.keymapper.base.logging.KeyMapperLoggingTree
-import io.github.sds100.keymapper.base.promode.SystemBridgeAutoStarter
+import io.github.sds100.keymapper.base.logging.SystemBridgeLogger
import io.github.sds100.keymapper.base.settings.Theme
import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl
import io.github.sds100.keymapper.base.system.notifications.NotificationController
@@ -90,6 +91,9 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
@Inject
lateinit var systemBridgeConnectionManager: SystemBridgeConnectionManagerImpl
+ @Inject
+ lateinit var systemBridgeLogger: SystemBridgeLogger
+
private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() }
private val userManager: UserManager? by lazy { getSystemService() }
@@ -105,12 +109,6 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
when (intent.action) {
Intent.ACTION_SHUTDOWN -> {
Timber.i("Clean shutdown")
- settingsRepository.set(Keys.isCleanShutdown, true)
-
- // Block until the value is persisted.
- runBlocking {
- settingsRepository.get(Keys.isCleanShutdown).first { it == true }
- }
}
}
}
@@ -192,20 +190,22 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
notificationController.init()
- processLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
- @Suppress("DEPRECATION")
- @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
- fun onResume() {
- // when the user returns to the app let everything know that the permissions could have changed
- notificationController.onOpenApp()
-
- if (BuildConfig.DEBUG &&
- permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)
- ) {
- accessibilityServiceAdapter.start()
+ processLifecycleOwner.lifecycle.addObserver(
+ object : LifecycleObserver {
+ @Suppress("DEPRECATION")
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ fun onResume() {
+ // when the user returns to the app let everything know that the permissions could have changed
+ notificationController.onOpenApp()
+
+ if (BuildConfig.DEBUG &&
+ permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)
+ ) {
+ accessibilityServiceAdapter.start()
+ }
}
- }
- })
+ },
+ )
appCoroutineScope.launch {
notificationController.openApp.collectLatest { intentAction ->
@@ -234,6 +234,11 @@ abstract class BaseKeyMapperApp : MultiDexApplication() {
if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) {
systemBridgeAutoStarter.init()
+ // Initialize SystemBridgeLogger to start receiving log messages from SystemBridge.
+ // Using Lazy<> to avoid circular dependency issues and ensure it's only created
+ // when the API level requirement is met.
+ systemBridgeLogger.start()
+
appCoroutineScope.launch {
systemBridgeConnectionManager.connectionState.collect { state ->
if (state is SystemBridgeConnectionState.Connected) {
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 d316fbc58a..b726317db1 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
@@ -48,7 +48,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-import timber.log.Timber
abstract class BaseMainActivity : AppCompatActivity() {
@@ -192,10 +191,6 @@ abstract class BaseMainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
- Timber.i(
- "MainActivity: onResume. Version: ${buildConfigProvider.version} ${buildConfigProvider.versionCode}",
- )
-
// This must be after onResume to ensure all the fragment lifecycles' have also
// resumed which are observing these events.
// This is checked here and not in KeyMapperApp's lifecycle observer because
@@ -259,7 +254,7 @@ abstract class BaseMainActivity : AppCompatActivity() {
}
ACTION_START_SYSTEM_BRIDGE -> {
- viewModel.launchProModeSetup()
+ viewModel.launchExpertModeSetup()
// Only clear the intent if it is handled in case it is used elsewhere
this.intent = null
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt
index 7dbe5c9a35..0b22cc858e 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt
@@ -25,11 +25,11 @@ import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementScreen
import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementViewModel
import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen
import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel
+import io.github.sds100.keymapper.base.expertmode.ExpertModeScreen
+import io.github.sds100.keymapper.base.expertmode.ExpertModeSetupScreen
import io.github.sds100.keymapper.base.logging.LogScreen
import io.github.sds100.keymapper.base.onboarding.HandleAccessibilityServiceDialogs
import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegateImpl
-import io.github.sds100.keymapper.base.promode.ProModeScreen
-import io.github.sds100.keymapper.base.promode.ProModeSetupScreen
import io.github.sds100.keymapper.base.settings.AutomaticChangeImeSettingsScreen
import io.github.sds100.keymapper.base.settings.DefaultOptionsSettingsScreen
import io.github.sds100.keymapper.base.settings.SettingsScreen
@@ -135,8 +135,8 @@ fun BaseMainNavHost(
)
}
- composable {
- ProModeScreen(
+ composable {
+ ExpertModeScreen(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(
@@ -151,8 +151,8 @@ fun BaseMainNavHost(
)
}
- composable {
- ProModeSetupScreen(
+ composable {
+ ExpertModeSetupScreen(
viewModel = hiltViewModel(),
)
}
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 dbc2efb661..435a7e3265 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
@@ -54,6 +54,8 @@ import io.github.sds100.keymapper.base.utils.ui.DialogProvider
import io.github.sds100.keymapper.base.utils.ui.DialogProviderImpl
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl
+import io.github.sds100.keymapper.common.utils.Clock
+import io.github.sds100.keymapper.common.utils.ClockImpl
import io.github.sds100.keymapper.common.utils.DefaultUuidGenerator
import io.github.sds100.keymapper.common.utils.UuidGenerator
import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter
@@ -202,4 +204,8 @@ abstract class BaseSingletonHiltModule {
abstract fun bindSetupAccessibilityServiceDelegate(
impl: SetupAccessibilityServiceDelegateImpl,
): SetupAccessibilityServiceDelegate
+
+ @Binds
+ @Singleton
+ abstract fun bindClock(impl: ClockImpl): Clock
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt
index 22a957ad16..75ebbc90c7 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt
@@ -25,6 +25,10 @@ import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCaseImpl
import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCase
import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCaseImpl
import io.github.sds100.keymapper.base.constraints.DisplayConstraintUseCase
+import io.github.sds100.keymapper.base.expertmode.ExpertModeSetupDelegateImpl
+import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupDelegate
+import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupUseCase
+import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupUseCaseImpl
import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase
import io.github.sds100.keymapper.base.home.ListKeyMapsUseCaseImpl
import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase
@@ -37,8 +41,6 @@ import io.github.sds100.keymapper.base.logging.ShareLogcatUseCase
import io.github.sds100.keymapper.base.logging.ShareLogcatUseCaseImpl
import io.github.sds100.keymapper.base.onboarding.OnboardingTipDelegate
import io.github.sds100.keymapper.base.onboarding.OnboardingTipDelegateImpl
-import io.github.sds100.keymapper.base.promode.SystemBridgeSetupUseCase
-import io.github.sds100.keymapper.base.promode.SystemBridgeSetupUseCaseImpl
import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCase
import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCaseImpl
import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase
@@ -151,7 +153,7 @@ abstract class BaseViewModelHiltModule {
@Binds
@ViewModelScoped
- abstract fun bindProModeSetupUseCase(
+ abstract fun bindExpertModeSetupUseCase(
impl: SystemBridgeSetupUseCaseImpl,
): SystemBridgeSetupUseCase
@@ -192,4 +194,10 @@ abstract class BaseViewModelHiltModule {
abstract fun bindFixKeyEventActionDelegate(
impl: FixKeyEventActionDelegateImpl,
): FixKeyEventActionDelegate
+
+ @Binds
+ @ViewModelScoped
+ abstract fun bindExpertModeSetupDelegate(
+ impl: ExpertModeSetupDelegateImpl,
+ ): SystemBridgeSetupDelegate
}
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 b051aa498d..0385eedbb0 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
@@ -121,7 +121,6 @@ object ActionUtils {
ActionId.SWIPE_SCREEN -> ActionCategory.INPUT
ActionId.PINCH_SCREEN -> ActionCategory.INPUT
ActionId.TEXT -> ActionCategory.INPUT
-
ActionId.OPEN_VOICE_ASSISTANT -> ActionCategory.APPS
ActionId.OPEN_DEVICE_ASSISTANT -> ActionCategory.APPS
ActionId.OPEN_CAMERA -> ActionCategory.APPS
@@ -132,30 +131,24 @@ object ActionUtils {
ActionId.URL -> ActionCategory.APPS
ActionId.HTTP_REQUEST -> ActionCategory.APPS
ActionId.SHELL_COMMAND -> ActionCategory.APPS
-
ActionId.TOGGLE_WIFI -> ActionCategory.CONNECTIVITY
ActionId.ENABLE_WIFI -> ActionCategory.CONNECTIVITY
ActionId.DISABLE_WIFI -> ActionCategory.CONNECTIVITY
-
ActionId.TOGGLE_BLUETOOTH -> ActionCategory.CONNECTIVITY
ActionId.ENABLE_BLUETOOTH -> ActionCategory.CONNECTIVITY
ActionId.DISABLE_BLUETOOTH -> ActionCategory.CONNECTIVITY
-
ActionId.TOGGLE_MOBILE_DATA -> ActionCategory.CONNECTIVITY
ActionId.ENABLE_MOBILE_DATA -> ActionCategory.CONNECTIVITY
ActionId.DISABLE_MOBILE_DATA -> ActionCategory.CONNECTIVITY
-
ActionId.TOGGLE_HOTSPOT -> ActionCategory.CONNECTIVITY
ActionId.ENABLE_HOTSPOT -> ActionCategory.CONNECTIVITY
ActionId.DISABLE_HOTSPOT -> ActionCategory.CONNECTIVITY
-
ActionId.TOGGLE_AUTO_BRIGHTNESS -> ActionCategory.DISPLAY
ActionId.DISABLE_AUTO_BRIGHTNESS -> ActionCategory.DISPLAY
ActionId.ENABLE_AUTO_BRIGHTNESS -> ActionCategory.DISPLAY
ActionId.INCREASE_BRIGHTNESS -> ActionCategory.DISPLAY
ActionId.DECREASE_BRIGHTNESS -> ActionCategory.DISPLAY
ActionId.SCREENSHOT -> ActionCategory.DISPLAY
-
ActionId.TOGGLE_AUTO_ROTATE -> ActionCategory.INTERFACE
ActionId.ENABLE_AUTO_ROTATE -> ActionCategory.INTERFACE
ActionId.DISABLE_AUTO_ROTATE -> ActionCategory.INTERFACE
@@ -163,7 +156,6 @@ object ActionUtils {
ActionId.LANDSCAPE_MODE -> ActionCategory.INTERFACE
ActionId.SWITCH_ORIENTATION -> ActionCategory.INTERFACE
ActionId.CYCLE_ROTATIONS -> ActionCategory.INTERFACE
-
ActionId.VOLUME_UP -> ActionCategory.VOLUME
ActionId.VOLUME_DOWN -> ActionCategory.VOLUME
ActionId.VOLUME_SHOW_DIALOG -> ActionCategory.VOLUME
@@ -181,13 +173,11 @@ object ActionUtils {
ActionId.MUTE_MICROPHONE -> ActionCategory.VOLUME
ActionId.UNMUTE_MICROPHONE -> ActionCategory.VOLUME
ActionId.TOGGLE_MUTE_MICROPHONE -> ActionCategory.VOLUME
-
ActionId.EXPAND_NOTIFICATION_DRAWER -> ActionCategory.NAVIGATION
ActionId.TOGGLE_NOTIFICATION_DRAWER -> ActionCategory.NAVIGATION
ActionId.EXPAND_QUICK_SETTINGS -> ActionCategory.NAVIGATION
ActionId.TOGGLE_QUICK_SETTINGS -> ActionCategory.NAVIGATION
ActionId.COLLAPSE_STATUS_BAR -> ActionCategory.NAVIGATION
-
ActionId.SOUND -> ActionCategory.MEDIA
ActionId.PAUSE_MEDIA -> ActionCategory.MEDIA
ActionId.PAUSE_MEDIA_PACKAGE -> ActionCategory.MEDIA
@@ -209,27 +199,22 @@ object ActionUtils {
ActionId.STEP_FORWARD_PACKAGE -> ActionCategory.MEDIA
ActionId.STEP_BACKWARD -> ActionCategory.MEDIA
ActionId.STEP_BACKWARD_PACKAGE -> ActionCategory.MEDIA
-
ActionId.GO_BACK -> ActionCategory.NAVIGATION
ActionId.GO_HOME -> ActionCategory.NAVIGATION
ActionId.OPEN_RECENTS -> ActionCategory.NAVIGATION
ActionId.TOGGLE_SPLIT_SCREEN -> ActionCategory.NAVIGATION
ActionId.GO_LAST_APP -> ActionCategory.NAVIGATION
ActionId.OPEN_MENU -> ActionCategory.NAVIGATION
-
ActionId.TOGGLE_FLASHLIGHT -> ActionCategory.FLASHLIGHT
ActionId.ENABLE_FLASHLIGHT -> ActionCategory.FLASHLIGHT
ActionId.DISABLE_FLASHLIGHT -> ActionCategory.FLASHLIGHT
ActionId.CHANGE_FLASHLIGHT_STRENGTH -> ActionCategory.FLASHLIGHT
-
ActionId.ENABLE_NFC -> ActionCategory.CONNECTIVITY
ActionId.DISABLE_NFC -> ActionCategory.CONNECTIVITY
ActionId.TOGGLE_NFC -> ActionCategory.CONNECTIVITY
-
ActionId.TOGGLE_AIRPLANE_MODE -> ActionCategory.CONNECTIVITY
ActionId.ENABLE_AIRPLANE_MODE -> ActionCategory.CONNECTIVITY
ActionId.DISABLE_AIRPLANE_MODE -> ActionCategory.CONNECTIVITY
-
ActionId.TEXT_CUT -> ActionCategory.KEYBOARD
ActionId.TEXT_COPY -> ActionCategory.KEYBOARD
ActionId.TEXT_PASTE -> ActionCategory.KEYBOARD
@@ -240,162 +225,283 @@ object ActionUtils {
ActionId.SHOW_KEYBOARD_PICKER -> ActionCategory.KEYBOARD
ActionId.SELECT_WORD_AT_CURSOR -> ActionCategory.KEYBOARD
ActionId.SWITCH_KEYBOARD -> ActionCategory.KEYBOARD
-
ActionId.LOCK_DEVICE -> ActionCategory.INTERFACE
ActionId.POWER_ON_OFF_DEVICE -> ActionCategory.INTERFACE
ActionId.SECURE_LOCK_DEVICE -> ActionCategory.INTERFACE
ActionId.SHOW_POWER_MENU -> ActionCategory.INTERFACE
-
ActionId.PHONE_CALL -> ActionCategory.TELEPHONY
ActionId.ANSWER_PHONE_CALL -> ActionCategory.TELEPHONY
ActionId.END_PHONE_CALL -> ActionCategory.TELEPHONY
ActionId.SEND_SMS -> ActionCategory.TELEPHONY
ActionId.COMPOSE_SMS -> ActionCategory.TELEPHONY
-
ActionId.DISMISS_MOST_RECENT_NOTIFICATION -> ActionCategory.NOTIFICATIONS
ActionId.DISMISS_ALL_NOTIFICATIONS -> ActionCategory.NOTIFICATIONS
ActionId.CREATE_NOTIFICATION -> ActionCategory.NOTIFICATIONS
ActionId.DEVICE_CONTROLS -> ActionCategory.APPS
-
ActionId.INTERACT_UI_ELEMENT -> ActionCategory.APPS
ActionId.FORCE_STOP_APP -> ActionCategory.APPS
ActionId.CLEAR_RECENT_APP -> ActionCategory.APPS
ActionId.MODIFY_SETTING -> ActionCategory.APPS
-
ActionId.CONSUME_KEY_EVENT -> ActionCategory.SPECIAL
}
@StringRes
fun getTitle(id: ActionId): Int = when (id) {
ActionId.TOGGLE_WIFI -> R.string.action_toggle_wifi
+
ActionId.ENABLE_WIFI -> R.string.action_enable_wifi
+
ActionId.DISABLE_WIFI -> R.string.action_disable_wifi
+
ActionId.TOGGLE_BLUETOOTH -> R.string.action_toggle_bluetooth
+
ActionId.ENABLE_BLUETOOTH -> R.string.action_enable_bluetooth
+
ActionId.DISABLE_BLUETOOTH -> R.string.action_disable_bluetooth
+
ActionId.TOGGLE_MOBILE_DATA -> R.string.action_toggle_mobile_data
+
ActionId.ENABLE_MOBILE_DATA -> R.string.action_enable_mobile_data
+
ActionId.DISABLE_MOBILE_DATA -> R.string.action_disable_mobile_data
+
ActionId.TOGGLE_AUTO_BRIGHTNESS -> R.string.action_toggle_auto_brightness
+
ActionId.DISABLE_AUTO_BRIGHTNESS -> R.string.action_disable_auto_brightness
+
ActionId.ENABLE_AUTO_BRIGHTNESS -> R.string.action_enable_auto_brightness
+
ActionId.INCREASE_BRIGHTNESS -> R.string.action_increase_brightness
+
ActionId.DECREASE_BRIGHTNESS -> R.string.action_decrease_brightness
+
ActionId.TOGGLE_AUTO_ROTATE -> R.string.action_toggle_auto_rotate
+
ActionId.ENABLE_AUTO_ROTATE -> R.string.action_enable_auto_rotate
+
ActionId.DISABLE_AUTO_ROTATE -> R.string.action_disable_auto_rotate
+
ActionId.PORTRAIT_MODE -> R.string.action_portrait_mode
+
ActionId.LANDSCAPE_MODE -> R.string.action_landscape_mode
+
ActionId.SWITCH_ORIENTATION -> R.string.action_switch_orientation
+
ActionId.CYCLE_ROTATIONS -> R.string.action_cycle_rotations
+
ActionId.VOLUME_UP -> R.string.action_volume_up
+
ActionId.VOLUME_DOWN -> R.string.action_volume_down
+
ActionId.VOLUME_SHOW_DIALOG -> R.string.action_volume_show_dialog
+
ActionId.VOLUME_DECREASE_STREAM -> R.string.action_decrease_stream
+
ActionId.VOLUME_INCREASE_STREAM -> R.string.action_increase_stream
+
ActionId.CYCLE_RINGER_MODE -> R.string.action_cycle_ringer_mode
+
ActionId.CHANGE_RINGER_MODE -> R.string.action_change_ringer_mode
+
ActionId.CYCLE_VIBRATE_RING -> R.string.action_cycle_vibrate_ring
+
ActionId.TOGGLE_DND_MODE -> R.string.action_toggle_dnd_mode
+
ActionId.ENABLE_DND_MODE -> R.string.action_enable_dnd_mode
+
ActionId.DISABLE_DND_MODE -> R.string.action_disable_dnd_mode
+
ActionId.VOLUME_UNMUTE -> R.string.action_volume_unmute
+
ActionId.VOLUME_MUTE -> R.string.action_volume_mute
+
ActionId.VOLUME_TOGGLE_MUTE -> R.string.action_toggle_mute
+
ActionId.MUTE_MICROPHONE -> R.string.action_mute_microphone
+
ActionId.UNMUTE_MICROPHONE -> R.string.action_unmute_microphone
+
ActionId.TOGGLE_MUTE_MICROPHONE -> R.string.action_toggle_mute_microphone
+
ActionId.EXPAND_NOTIFICATION_DRAWER -> R.string.action_expand_notification_drawer
+
ActionId.TOGGLE_NOTIFICATION_DRAWER -> R.string.action_toggle_notification_drawer
+
ActionId.EXPAND_QUICK_SETTINGS -> R.string.action_expand_quick_settings
+
ActionId.TOGGLE_QUICK_SETTINGS -> R.string.action_toggle_quick_settings
+
ActionId.COLLAPSE_STATUS_BAR -> R.string.action_collapse_status_bar
+
ActionId.PAUSE_MEDIA -> R.string.action_pause_media
+
ActionId.PAUSE_MEDIA_PACKAGE -> R.string.action_pause_media_package
+
ActionId.PLAY_MEDIA -> R.string.action_play_media
+
ActionId.PLAY_MEDIA_PACKAGE -> R.string.action_play_media_package
+
ActionId.PLAY_PAUSE_MEDIA -> R.string.action_play_pause_media
+
ActionId.PLAY_PAUSE_MEDIA_PACKAGE -> R.string.action_play_pause_media_package
+
ActionId.NEXT_TRACK -> R.string.action_next_track
+
ActionId.NEXT_TRACK_PACKAGE -> R.string.action_next_track_package
+
ActionId.PREVIOUS_TRACK -> R.string.action_previous_track
+
ActionId.PREVIOUS_TRACK_PACKAGE -> R.string.action_previous_track_package
+
ActionId.FAST_FORWARD -> R.string.action_fast_forward
+
ActionId.FAST_FORWARD_PACKAGE -> R.string.action_fast_forward_package
+
ActionId.REWIND -> R.string.action_rewind
+
ActionId.REWIND_PACKAGE -> R.string.action_rewind_package
+
ActionId.STOP_MEDIA -> R.string.action_stop_media
+
ActionId.STOP_MEDIA_PACKAGE -> R.string.action_stop_media_package
+
ActionId.STEP_FORWARD -> R.string.action_step_forward_media
+
ActionId.STEP_FORWARD_PACKAGE -> R.string.action_step_forward_media_package
+
ActionId.STEP_BACKWARD -> R.string.action_step_backward_media
+
ActionId.STEP_BACKWARD_PACKAGE -> R.string.action_step_backward_media_package
+
ActionId.GO_BACK -> R.string.action_go_back
+
ActionId.GO_HOME -> R.string.action_go_home
+
ActionId.OPEN_RECENTS -> R.string.action_open_recents
+
ActionId.TOGGLE_SPLIT_SCREEN -> R.string.action_toggle_split_screen
+
ActionId.GO_LAST_APP -> R.string.action_go_last_app
+
ActionId.OPEN_MENU -> R.string.action_open_menu
+
ActionId.TOGGLE_FLASHLIGHT -> R.string.action_toggle_flashlight
+
ActionId.ENABLE_FLASHLIGHT -> R.string.action_enable_flashlight
+
ActionId.DISABLE_FLASHLIGHT -> R.string.action_disable_flashlight
+
ActionId.CHANGE_FLASHLIGHT_STRENGTH -> R.string.action_flashlight_change_strength
+
ActionId.ENABLE_NFC -> R.string.action_nfc_enable
+
ActionId.DISABLE_NFC -> R.string.action_nfc_disable
+
ActionId.TOGGLE_NFC -> R.string.action_nfc_toggle
+
ActionId.MOVE_CURSOR -> R.string.action_move_cursor
+
ActionId.TOGGLE_KEYBOARD -> R.string.action_toggle_keyboard
+
ActionId.SHOW_KEYBOARD -> R.string.action_show_keyboard
+
ActionId.HIDE_KEYBOARD -> R.string.action_hide_keyboard
+
ActionId.SHOW_KEYBOARD_PICKER -> R.string.action_show_keyboard_picker
+
ActionId.TEXT_CUT -> R.string.action_text_cut
+
ActionId.TEXT_COPY -> R.string.action_text_copy
+
ActionId.TEXT_PASTE -> R.string.action_text_paste
+
ActionId.SELECT_WORD_AT_CURSOR -> R.string.action_select_word_at_cursor
+
ActionId.SWITCH_KEYBOARD -> R.string.action_switch_keyboard
+
ActionId.TOGGLE_AIRPLANE_MODE -> R.string.action_toggle_airplane_mode
+
ActionId.ENABLE_AIRPLANE_MODE -> R.string.action_enable_airplane_mode
+
ActionId.DISABLE_AIRPLANE_MODE -> R.string.action_disable_airplane_mode
+
ActionId.SCREENSHOT -> R.string.action_screenshot
+
ActionId.OPEN_VOICE_ASSISTANT -> R.string.action_open_assistant
+
ActionId.OPEN_DEVICE_ASSISTANT -> R.string.action_open_device_assistant
+
ActionId.OPEN_CAMERA -> R.string.action_open_camera
+
ActionId.LOCK_DEVICE -> R.string.action_lock_device
+
ActionId.POWER_ON_OFF_DEVICE -> R.string.action_power_on_off_device
+
ActionId.SECURE_LOCK_DEVICE -> R.string.action_secure_lock_device
+
ActionId.CONSUME_KEY_EVENT -> R.string.action_consume_keyevent
+
ActionId.OPEN_SETTINGS -> R.string.action_open_settings
+
ActionId.SHOW_POWER_MENU -> R.string.action_show_power_menu
+
ActionId.APP -> R.string.action_open_app
+
ActionId.APP_SHORTCUT -> R.string.action_open_app_shortcut
+
ActionId.KEY_CODE -> R.string.action_input_key_code
+
ActionId.KEY_EVENT -> R.string.action_input_key_event
+
ActionId.TAP_SCREEN -> R.string.action_tap_screen
+
ActionId.SWIPE_SCREEN -> R.string.action_swipe_screen
+
ActionId.PINCH_SCREEN -> R.string.action_pinch_screen
+
ActionId.TEXT -> R.string.action_input_text
+
ActionId.URL -> R.string.action_open_url
+
ActionId.INTENT -> R.string.action_send_intent
+
ActionId.PHONE_CALL -> R.string.action_phone_call
+
ActionId.SOUND -> R.string.action_play_sound
+
ActionId.DISMISS_MOST_RECENT_NOTIFICATION ->
R.string.action_dismiss_most_recent_notification
+
ActionId.DISMISS_ALL_NOTIFICATIONS -> R.string.action_dismiss_all_notifications
+
ActionId.CREATE_NOTIFICATION -> R.string.action_create_notification
+
ActionId.ANSWER_PHONE_CALL -> R.string.action_answer_call
+
ActionId.END_PHONE_CALL -> R.string.action_end_call
+
ActionId.SEND_SMS -> R.string.action_send_sms
+
ActionId.COMPOSE_SMS -> R.string.action_compose_sms
+
ActionId.DEVICE_CONTROLS -> R.string.action_device_controls
+
ActionId.HTTP_REQUEST -> R.string.action_http_request
+
ActionId.SHELL_COMMAND -> R.string.action_shell_command
+
ActionId.INTERACT_UI_ELEMENT -> R.string.action_interact_ui_element_title
+
ActionId.FORCE_STOP_APP -> R.string.action_force_stop_app
+
ActionId.CLEAR_RECENT_APP -> R.string.action_clear_recent_app
ActionId.MODIFY_SETTING -> R.string.action_modify_setting
+
ActionId.TOGGLE_HOTSPOT -> R.string.action_toggle_hotspot
+
ActionId.ENABLE_HOTSPOT -> R.string.action_enable_hotspot
+
ActionId.DISABLE_HOTSPOT -> R.string.action_disable_hotspot
}
@@ -525,10 +631,13 @@ object ActionUtils {
fun getMinApi(id: ActionId): Int = when (id) {
ActionId.ANSWER_PHONE_CALL -> Build.VERSION_CODES.O
+
ActionId.END_PHONE_CALL -> Build.VERSION_CODES.P
ActionId.TOGGLE_SPLIT_SCREEN -> Build.VERSION_CODES.N
+
ActionId.GO_LAST_APP -> Build.VERSION_CODES.N
+
ActionId.TAP_SCREEN -> Build.VERSION_CODES.N
ActionId.VOLUME_MUTE,
@@ -562,6 +671,7 @@ object ActionUtils {
-> Build.VERSION_CODES.JELLY_BEAN_MR2
ActionId.SHOW_POWER_MENU -> Build.VERSION_CODES.LOLLIPOP
+
ActionId.DEVICE_CONTROLS -> Build.VERSION_CODES.S
// It could be supported on older versions but system bridge min API is Q and its extra
@@ -761,6 +871,7 @@ object ActionUtils {
}
ActionId.SECURE_LOCK_DEVICE -> return listOf(Permission.DEVICE_ADMIN)
+
ActionId.POWER_ON_OFF_DEVICE -> return if (isSystemBridgeSupported) {
emptyList()
} else {
@@ -923,7 +1034,6 @@ object ActionUtils {
ActionId.INTERACT_UI_ELEMENT -> KeyMapperIcons.JumpToElement
ActionId.FORCE_STOP_APP -> Icons.Outlined.Dangerous
ActionId.CLEAR_RECENT_APP -> Icons.Outlined.VerticalSplit
-
ActionId.MODIFY_SETTING -> Icons.Outlined.Settings
ActionId.TOGGLE_HOTSPOT -> Icons.Outlined.WifiTethering
ActionId.ENABLE_HOTSPOT -> Icons.Outlined.WifiTethering
@@ -933,6 +1043,7 @@ object ActionUtils {
fun ActionData.canBeHeldDown(): Boolean = when (this) {
is ActionData.InputKeyEvent -> true
+ is ActionData.TapScreen -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
else -> false
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt
index 55c61681ab..b7fec30f2c 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt
@@ -79,11 +79,11 @@ fun ActionsScreen(modifier: Modifier = Modifier, viewModel: ConfigActionsViewMod
sheetState = sheetState,
onDismissRequest = viewModel::dismissFixKeyEventActionBottomSheet,
onEnableAccessibilityServiceClick = viewModel::onEnableAccessibilityServiceClick,
- onEnableProModeClick = viewModel::onEnableProModeForKeyEventActionsClick,
+ onEnableExpertModeClick = viewModel::onEnableExpertModeForKeyEventActionsClick,
onEnableInputMethodClick = viewModel::onEnableImeClick,
onChooseInputMethodClick = viewModel::onChooseImeClick,
onDoneClick = viewModel::dismissFixKeyEventActionBottomSheet,
- onSelectProMode = viewModel::onSelectProMode,
+ onSelectExpertMode = viewModel::onSelectExpertMode,
onSelectInputMethod = viewModel::onSelectInputMethod,
onAutoSwitchImeCheckedChange = viewModel::onAutoSwitchImeCheckedChange,
)
@@ -150,6 +150,7 @@ private fun ActionsScreen(
when (state) {
State.Loading -> Loading()
+
is State.Data -> Surface(modifier = modifier) {
Column {
Spacer(Modifier.height(8.dp))
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt
index 954e188961..e8262c6c42 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt
@@ -1,17 +1,18 @@
package io.github.sds100.keymapper.base.actions
import android.os.Build
-import android.util.Base64
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import io.github.sds100.keymapper.base.utils.ProModeStatus
+import io.github.sds100.keymapper.base.R
+import io.github.sds100.keymapper.base.utils.ExpertModeStatus
import io.github.sds100.keymapper.base.utils.navigation.NavDestination
import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
import io.github.sds100.keymapper.base.utils.navigation.navigate
+import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
import io.github.sds100.keymapper.common.models.ShellExecutionMode
import io.github.sds100.keymapper.common.models.isExecuting
import io.github.sds100.keymapper.common.utils.Constants
@@ -20,6 +21,7 @@ import io.github.sds100.keymapper.data.Keys
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
+import java.util.Base64
import javax.inject.Inject
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.map
@@ -32,7 +34,9 @@ class ConfigShellCommandViewModel @Inject constructor(
private val navigationProvider: NavigationProvider,
private val systemBridgeConnectionManager: SystemBridgeConnectionManager,
private val preferenceRepository: PreferenceRepository,
-) : ViewModel() {
+ resourceProvider: ResourceProvider,
+) : ViewModel(),
+ ResourceProvider by resourceProvider {
var state: ShellCommandActionState by mutableStateOf(ShellCommandActionState())
private set
@@ -40,16 +44,16 @@ class ConfigShellCommandViewModel @Inject constructor(
private var testJob: Job? = null
init {
- // Update ProModeStatus in state
+ // Update ExpertModeStatus in state
if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) {
viewModelScope.launch {
systemBridgeConnectionManager.connectionState.map { connectionState ->
when (connectionState) {
- is SystemBridgeConnectionState.Connected -> ProModeStatus.ENABLED
- is SystemBridgeConnectionState.Disconnected -> ProModeStatus.DISABLED
+ is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED
+ is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED
}
- }.collect { proModeStatus ->
- state = state.copy(proModeStatus = proModeStatus)
+ }.collect { expertModeStatus ->
+ state = state.copy(expertModeStatus = expertModeStatus)
}
}
}
@@ -68,11 +72,11 @@ class ConfigShellCommandViewModel @Inject constructor(
}
fun onDescriptionChanged(newDescription: String) {
- state = state.copy(description = newDescription)
+ state = state.copy(description = newDescription, descriptionError = null)
}
fun onCommandChanged(newCommand: String) {
- state = state.copy(command = newCommand)
+ state = state.copy(command = newCommand, commandError = null)
saveScriptText(newCommand)
}
@@ -84,9 +88,15 @@ class ConfigShellCommandViewModel @Inject constructor(
state = state.copy(timeoutSeconds = newTimeoutSeconds)
}
- fun onTestClick() {
+ fun onTestClick(): Boolean {
testJob?.cancel()
+ val commandError = validateCommand(state.command)
+ if (commandError != null) {
+ state = state.copy(commandError = commandError)
+ return false
+ }
+
state = state.copy(
isRunning = true,
testResult = null,
@@ -95,6 +105,8 @@ class ConfigShellCommandViewModel @Inject constructor(
testJob = viewModelScope.launch {
testCommand()
}
+
+ return true
}
private suspend fun testCommand() {
@@ -119,7 +131,21 @@ class ConfigShellCommandViewModel @Inject constructor(
)
}
- fun onDoneClick() {
+ fun onDoneClick(): Boolean {
+ val commandError = validateCommand(state.command)
+ if (commandError != null) {
+ state = state.copy(commandError = commandError)
+ return false
+ }
+
+ if (state.description.isBlank()) {
+ state = state.copy(
+ descriptionError = getString(R.string.error_cant_be_empty),
+ )
+
+ return false
+ }
+
val action = ActionData.ShellCommand(
description = state.description,
command = state.command,
@@ -133,6 +159,8 @@ class ConfigShellCommandViewModel @Inject constructor(
viewModelScope.launch {
navigationProvider.popBackStackWithResult(Json.encodeToString(action))
}
+
+ return true
}
fun onCancelClick() {
@@ -144,15 +172,33 @@ class ConfigShellCommandViewModel @Inject constructor(
}
}
- fun onSetupProModeClick() {
+ fun onSetupExpertModeClick() {
viewModelScope.launch {
- navigationProvider.navigate("shell_command_setup_pro_mode", NavDestination.ProModeSetup)
+ navigationProvider.navigate(
+ "shell_command_setup_expert_mode",
+ NavDestination.ExpertModeSetup,
+ )
+ }
+ }
+
+ /**
+ * @return the error message.
+ */
+ private fun validateCommand(command: String): String? {
+ if (state.command.isBlank()) {
+ return getString(R.string.action_shell_command_command_empty_error)
+ }
+
+ if (state.command.trimStart().startsWith("adb shell")) {
+ return getString(R.string.action_shell_command_adb_shell_error)
}
+
+ return null
}
private fun saveScriptText(scriptText: String) {
viewModelScope.launch {
- val encodedText = Base64.encodeToString(scriptText.toByteArray(), Base64.DEFAULT).trim()
+ val encodedText = Base64.getEncoder().encodeToString(scriptText.toByteArray()).trim()
preferenceRepository.set(Keys.shellCommandScriptText, encodedText)
}
}
@@ -162,7 +208,7 @@ class ConfigShellCommandViewModel @Inject constructor(
preferenceRepository.get(Keys.shellCommandScriptText).collect { savedScriptText ->
if (savedScriptText != null && state.command.isEmpty()) {
try {
- val decodedText = String(Base64.decode(savedScriptText, Base64.DEFAULT))
+ val decodedText = String(Base64.getDecoder().decode(savedScriptText))
state = state.copy(command = decodedText)
} catch (e: Exception) {
// If decoding fails, ignore the saved text
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
new file mode 100644
index 0000000000..f478803eba
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionTriggerDevice.kt
@@ -0,0 +1,13 @@
+package io.github.sds100.keymapper.base.actions
+
+/**
+ * Identifies which device triggered the action.
+ */
+sealed class PerformActionTriggerDevice {
+ /**
+ * The action was triggered by an evdev-level input device.
+ */
+ data class Evdev(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 25e9764bf9..8b1dd81c9d 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
@@ -28,16 +28,13 @@ import io.github.sds100.keymapper.common.utils.KMError.SdkVersionTooLow
import io.github.sds100.keymapper.common.utils.KMResult
import io.github.sds100.keymapper.common.utils.Orientation
import io.github.sds100.keymapper.common.utils.Success
-import io.github.sds100.keymapper.common.utils.dataOrNull
import io.github.sds100.keymapper.common.utils.firstBlocking
import io.github.sds100.keymapper.common.utils.getWordBoundaries
-import io.github.sds100.keymapper.common.utils.ifIsData
import io.github.sds100.keymapper.common.utils.onFailure
import io.github.sds100.keymapper.common.utils.onSuccess
import io.github.sds100.keymapper.common.utils.otherwise
import io.github.sds100.keymapper.common.utils.success
import io.github.sds100.keymapper.common.utils.then
-import io.github.sds100.keymapper.common.utils.withFlag
import io.github.sds100.keymapper.data.Keys
import io.github.sds100.keymapper.data.PreferenceDefaults
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
@@ -52,7 +49,6 @@ import io.github.sds100.keymapper.system.devices.DevicesAdapter
import io.github.sds100.keymapper.system.display.DisplayAdapter
import io.github.sds100.keymapper.system.files.FileAdapter
import io.github.sds100.keymapper.system.files.FileUtils
-import io.github.sds100.keymapper.system.inputevents.KeyEventUtils
import io.github.sds100.keymapper.system.inputevents.Scancode
import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter
import io.github.sds100.keymapper.system.intents.IntentAdapter
@@ -78,12 +74,9 @@ import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
@@ -143,15 +136,19 @@ class PerformActionsUseCaseImpl @AssistedInject constructor(
)
}
- private val injectKeyEventsWithSystemBridge: StateFlow =
- settingsRepository.get(Keys.keyEventActionsUseSystemBridge)
- .map { it ?: PreferenceDefaults.KEY_EVENT_ACTIONS_USE_SYSTEM_BRIDGE }
- .stateIn(coroutineScope, SharingStarted.Eagerly, false)
+ private val performKeyEventActionDelegate: PerformKeyEventActionDelegate =
+ PerformKeyEventActionDelegate(
+ coroutineScope,
+ settingsRepository,
+ inputEventHub,
+ devicesAdapter,
+ )
override suspend fun perform(
action: ActionData,
inputEventAction: InputEventAction,
keyMetaState: Int,
+ device: PerformActionTriggerDevice,
) {
/**
* Is null if the action is being performed asynchronously
@@ -172,48 +169,12 @@ class PerformActionsUseCaseImpl @AssistedInject constructor(
}
is ActionData.InputKeyEvent -> {
- val deviceId: Int = getDeviceIdForKeyEventAction(action)
-
- // See issue #1683. Some apps ignore key events which do not have a source.
- val source = when {
- KeyEventUtils.isDpadKeyCode(action.keyCode) -> InputDevice.SOURCE_DPAD
- KeyEventUtils.isGamepadButton(action.keyCode) -> InputDevice.SOURCE_GAMEPAD
- else -> InputDevice.SOURCE_KEYBOARD
- }
-
- val firstInputAction = if (inputEventAction == InputEventAction.UP) {
- KeyEvent.ACTION_UP
- } else {
- KeyEvent.ACTION_DOWN
- }
-
- val model = InjectKeyEventModel(
- keyCode = action.keyCode,
- action = firstInputAction,
- metaState = keyMetaState.withFlag(action.metaState),
- deviceId = deviceId,
- source = source,
- repeatCount = 0,
- scanCode = 0,
+ result = performKeyEventActionDelegate.perform(
+ action,
+ inputEventAction,
+ keyMetaState,
+ device,
)
-
- if (inputEventAction == InputEventAction.DOWN_UP) {
- result = inputEventHub.injectKeyEvent(
- model,
- useSystemBridgeIfAvailable = injectKeyEventsWithSystemBridge.value,
- )
- .then {
- inputEventHub.injectKeyEvent(
- model.copy(action = KeyEvent.ACTION_UP),
- useSystemBridgeIfAvailable = injectKeyEventsWithSystemBridge.value,
- )
- }
- } else {
- result = inputEventHub.injectKeyEvent(
- model,
- useSystemBridgeIfAvailable = injectKeyEventsWithSystemBridge.value,
- )
- }
}
is ActionData.PhoneCall -> {
@@ -1108,53 +1069,6 @@ class PerformActionsUseCaseImpl @AssistedInject constructor(
.map { it ?: PreferenceDefaults.HOLD_DOWN_DURATION }
.map { it.toLong() }
- private fun getDeviceIdForKeyEventAction(action: ActionData.InputKeyEvent): Int {
- if (action.device?.descriptor == null) {
- // automatically select a game controller as the input device for game controller key events
-
- if (KeyEventUtils.isGamepadKeyCode(action.keyCode)) {
- devicesAdapter.connectedInputDevices.value.ifIsData { inputDevices ->
- val device = inputDevices.find { it.isGameController }
-
- if (device != null) {
- return device.id
- }
- }
- }
-
- return 0
- }
-
- val inputDevices = devicesAdapter.connectedInputDevices.value
-
- val devicesWithSameDescriptor =
- inputDevices.dataOrNull()
- ?.filter { it.descriptor == action.device.descriptor }
- ?: emptyList()
-
- if (devicesWithSameDescriptor.isEmpty()) {
- return -1
- }
-
- if (devicesWithSameDescriptor.size == 1) {
- return devicesWithSameDescriptor[0].id
- }
-
- /*
- if there are multiple devices use the device that supports the key
- code. if none do then use the first one
- */
- val deviceThatHasKey = devicesWithSameDescriptor.singleOrNull {
- devicesAdapter.deviceHasKey(it.id, action.keyCode)
- }
-
- val device = deviceThatHasKey
- ?: devicesWithSameDescriptor.singleOrNull { it.name == action.device.name }
- ?: devicesWithSameDescriptor[0]
-
- return device.id
- }
-
private fun closeStatusBarShade(): KMResult<*> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return service
@@ -1233,6 +1147,7 @@ interface PerformActionsUseCase {
action: ActionData,
inputEventAction: InputEventAction = InputEventAction.DOWN_UP,
keyMetaState: Int = 0,
+ device: PerformActionTriggerDevice = PerformActionTriggerDevice.Default,
)
fun getErrorSnapshot(): ActionErrorSnapshot
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
new file mode 100644
index 0000000000..a059059ec3
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegate.kt
@@ -0,0 +1,206 @@
+package io.github.sds100.keymapper.base.actions
+
+import android.view.InputDevice
+import android.view.KeyEvent
+import io.github.sds100.keymapper.base.input.InjectKeyEventModel
+import io.github.sds100.keymapper.base.input.InputEventHub
+import io.github.sds100.keymapper.common.utils.InputEventAction
+import io.github.sds100.keymapper.common.utils.KMResult
+import io.github.sds100.keymapper.common.utils.dataOrNull
+import io.github.sds100.keymapper.common.utils.ifIsData
+import io.github.sds100.keymapper.common.utils.then
+import io.github.sds100.keymapper.common.utils.withFlag
+import io.github.sds100.keymapper.data.Keys
+import io.github.sds100.keymapper.data.PreferenceDefaults
+import io.github.sds100.keymapper.data.repositories.PreferenceRepository
+import io.github.sds100.keymapper.system.devices.DevicesAdapter
+import io.github.sds100.keymapper.system.inputevents.KeyEventUtils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+class PerformKeyEventActionDelegate(
+ private val coroutineScope: CoroutineScope,
+ private val settingsRepository: PreferenceRepository,
+ private val inputEventHub: InputEventHub,
+ private val devicesAdapter: DevicesAdapter,
+) {
+ private val injectKeyEventsWithSystemBridge: StateFlow =
+ settingsRepository.get(Keys.keyEventActionsUseSystemBridge)
+ .map { it ?: PreferenceDefaults.KEY_EVENT_ACTIONS_USE_SYSTEM_BRIDGE }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, false)
+
+ suspend fun perform(
+ action: ActionData.InputKeyEvent,
+ inputEventAction: InputEventAction,
+ keyMetaState: Int,
+ triggerDevice: PerformActionTriggerDevice,
+ ): KMResult {
+ // Only input evdev event if the device is grabbed. It may not be grabbed
+ // if the device is disconnected.
+ if (injectKeyEventsWithSystemBridge.value &&
+ triggerDevice is PerformActionTriggerDevice.Evdev &&
+ (action.device == null || isActionDeviceGrabbed(action.device))
+ ) {
+ return injectEvdevEvent(inputEventAction, triggerDevice.deviceId, action)
+ }
+
+ val deviceId: Int = getDeviceIdForKeyEventAction(action)
+
+ // If the device that the user specified in the action can not be found
+ // then fallback to evdev injection.
+ if (injectKeyEventsWithSystemBridge.value &&
+ deviceId == -1 &&
+ triggerDevice is PerformActionTriggerDevice.Evdev
+ ) {
+ return injectEvdevEvent(inputEventAction, triggerDevice.deviceId, action)
+ }
+
+ // See issue #1683. Some apps ignore key events which do not have a source.
+ val source = when {
+ KeyEventUtils.isDpadKeyCode(action.keyCode) -> InputDevice.SOURCE_DPAD
+ KeyEventUtils.isGamepadButton(action.keyCode) -> InputDevice.SOURCE_GAMEPAD
+ else -> InputDevice.SOURCE_KEYBOARD
+ }
+
+ val firstInputAction = if (inputEventAction == InputEventAction.UP) {
+ KeyEvent.ACTION_UP
+ } else {
+ KeyEvent.ACTION_DOWN
+ }
+
+ val model = InjectKeyEventModel(
+ keyCode = action.keyCode,
+ action = firstInputAction,
+ metaState = keyMetaState.withFlag(action.metaState),
+ deviceId = deviceId,
+ source = source,
+ repeatCount = 0,
+ scanCode = 0,
+ )
+
+ return injectAndroidKeyEvent(inputEventAction, model)
+ }
+
+ private fun isActionDeviceGrabbed(device: ActionData.InputKeyEvent.Device): Boolean {
+ return inputEventHub.getGrabbedDevices().any { it.name == device.name }
+ }
+
+ private fun injectEvdevEvent(
+ inputEventAction: InputEventAction,
+ deviceId: Int,
+ action: ActionData.InputKeyEvent,
+ ): KMResult {
+ when (inputEventAction) {
+ InputEventAction.DOWN_UP -> {
+ return injectDownEvdevEvent(
+ deviceId,
+ action,
+ ).then { injectUpEvdevEvent(deviceId, action) }
+ }
+
+ InputEventAction.DOWN -> return injectDownEvdevEvent(
+ deviceId,
+ action,
+ )
+
+ InputEventAction.UP -> return injectUpEvdevEvent(
+ deviceId,
+ action,
+ )
+ }
+ }
+
+ private fun injectDownEvdevEvent(
+ deviceId: Int,
+ action: ActionData.InputKeyEvent,
+ ): KMResult {
+ return inputEventHub.injectEvdevEventKeyCode(
+ deviceId = deviceId,
+ keyCode = action.keyCode,
+ value = 1,
+ )
+ }
+
+ private fun injectUpEvdevEvent(
+ deviceId: Int,
+ action: ActionData.InputKeyEvent,
+ ): KMResult {
+ return inputEventHub.injectEvdevEventKeyCode(
+ deviceId = deviceId,
+ action.keyCode,
+ value = 0,
+ )
+ }
+
+ private suspend fun injectAndroidKeyEvent(
+ inputEventAction: InputEventAction,
+ model: InjectKeyEventModel,
+ ): KMResult {
+ if (inputEventAction == InputEventAction.DOWN_UP) {
+ return inputEventHub.injectKeyEvent(
+ model,
+ useSystemBridgeIfAvailable = injectKeyEventsWithSystemBridge.value,
+ ).then {
+ inputEventHub.injectKeyEvent(
+ model.copy(action = KeyEvent.ACTION_UP),
+ useSystemBridgeIfAvailable = injectKeyEventsWithSystemBridge.value,
+ )
+ }
+ } else {
+ return inputEventHub.injectKeyEvent(
+ model,
+ useSystemBridgeIfAvailable = injectKeyEventsWithSystemBridge.value,
+ )
+ }
+ }
+
+ private fun getDeviceIdForKeyEventAction(action: ActionData.InputKeyEvent): Int {
+ if (action.device?.descriptor == null) {
+ // automatically select a game controller as the input device for game controller key events
+
+ if (KeyEventUtils.isGamepadKeyCode(action.keyCode)) {
+ devicesAdapter.connectedInputDevices.value.ifIsData { inputDevices ->
+ val device = inputDevices.find { it.isGameController }
+
+ if (device != null) {
+ return device.id
+ }
+ }
+ }
+
+ return 0
+ }
+
+ val inputDevices = devicesAdapter.connectedInputDevices.value
+
+ val devicesWithSameDescriptor =
+ inputDevices.dataOrNull()
+ ?.filter { it.descriptor == action.device.descriptor }
+ ?: emptyList()
+
+ if (devicesWithSameDescriptor.isEmpty()) {
+ return -1
+ }
+
+ if (devicesWithSameDescriptor.size == 1) {
+ return devicesWithSameDescriptor[0].id
+ }
+
+ /*
+ if there are multiple devices use the device that supports the key
+ code. if none do then use the first one
+ */
+ val deviceThatHasKey = devicesWithSameDescriptor.singleOrNull {
+ devicesAdapter.deviceHasKey(it.id, action.keyCode)
+ }
+
+ val device = deviceThatHasKey
+ ?: devicesWithSameDescriptor.singleOrNull { it.name == action.device.name }
+ ?: devicesWithSameDescriptor[0]
+
+ return device.id
+ }
+}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ShellCommandActionScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ShellCommandActionScreen.kt
index 7df39b3771..1523ebb6cb 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ShellCommandActionScreen.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ShellCommandActionScreen.kt
@@ -38,11 +38,7 @@ import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -54,7 +50,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.compose.KeyMapperTheme
-import io.github.sds100.keymapper.base.utils.ProModeStatus
+import io.github.sds100.keymapper.base.utils.ExpertModeStatus
import io.github.sds100.keymapper.base.utils.getFullMessage
import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow
import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText
@@ -71,7 +67,9 @@ import kotlinx.coroutines.launch
data class ShellCommandActionState(
val description: String = "",
+ val descriptionError: String? = null,
val command: String = "",
+ val commandError: String? = null,
val executionMode: ShellExecutionMode = ShellExecutionMode.STANDARD,
/**
* UI works with seconds for user-friendliness
@@ -79,7 +77,7 @@ data class ShellCommandActionState(
val timeoutSeconds: Int = 10,
val isRunning: Boolean = false,
val testResult: KMResult? = null,
- val proModeStatus: ProModeStatus = ProModeStatus.UNSUPPORTED,
+ val expertModeStatus: ExpertModeStatus = ExpertModeStatus.UNSUPPORTED,
)
@Composable
@@ -98,7 +96,7 @@ fun ShellCommandActionScreen(
onKillClick = viewModel::onKillClick,
onDoneClick = viewModel::onDoneClick,
onCancelClick = viewModel::onCancelClick,
- onSetupProModeClick = viewModel::onSetupProModeClick,
+ onSetupExpertModeClick = viewModel::onSetupExpertModeClick,
)
}
@@ -111,20 +109,21 @@ private fun ShellCommandActionScreen(
onCommandChanged: (String) -> Unit = {},
onExecutionModeChanged: (ShellExecutionMode) -> Unit = {},
onTimeoutChanged: (Int) -> Unit = {},
- onTestClick: () -> Unit = {},
+ /**
+ * Returns whether validation passed
+ */
+ onTestClick: () -> Boolean = { true },
onKillClick: () -> Unit = {},
- onDoneClick: () -> Unit = {},
+ /**
+ * Returns whether validation passed
+ */
+ onDoneClick: () -> Boolean = { true },
onCancelClick: () -> Unit = {},
- onSetupProModeClick: () -> Unit = {},
+ onSetupExpertModeClick: () -> Unit = {},
) {
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
- var descriptionError: String? by rememberSaveable { mutableStateOf(null) }
- var commandError: String? by rememberSaveable { mutableStateOf(null) }
- val descriptionEmptyErrorString = stringResource(R.string.error_cant_be_empty)
- val commandEmptyErrorString = stringResource(R.string.action_shell_command_command_empty_error)
-
Scaffold(
modifier = modifier,
topBar = {
@@ -137,24 +136,11 @@ private fun ShellCommandActionScreen(
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
- var hasError = false
-
- if (state.description.isBlank()) {
- descriptionError = descriptionEmptyErrorString
- hasError = true
- }
-
- if (state.command.isBlank()) {
- commandError = commandEmptyErrorString
- hasError = true
- }
-
- if (hasError) {
+ // Go to the configuration tab if validation failed
+ if (!onDoneClick()) {
scope.launch {
scrollState.animateScrollTo(0)
}
- } else {
- onDoneClick()
}
},
text = { Text(stringResource(R.string.pos_done)) },
@@ -226,29 +212,20 @@ private fun ShellCommandActionScreen(
0 -> ShellCommandConfigurationContent(
modifier = Modifier.fillMaxSize(),
state = state,
- descriptionError = descriptionError,
- commandError = commandError,
- onDescriptionChanged = {
- descriptionError = null
- onDescriptionChanged(it)
- },
- onCommandChanged = {
- commandError = null
- onCommandChanged(it)
- },
+ descriptionError = state.descriptionError,
+ commandError = state.commandError,
+ onDescriptionChanged = onDescriptionChanged,
+ onCommandChanged = onCommandChanged,
onExecutionModeChanged = onExecutionModeChanged,
onTimeoutChanged = onTimeoutChanged,
onTestClick = {
- if (state.command.isBlank()) {
- commandError = commandEmptyErrorString
- } else {
- onTestClick()
+ if (onTestClick()) {
scope.launch {
pagerState.animateScrollToPage(1) // Switch to output tab
}
}
},
- onSetupProModeClick = onSetupProModeClick,
+ onSetupExpertModeClick = onSetupExpertModeClick,
)
1 -> ShellCommandOutputContent(
@@ -273,7 +250,7 @@ private fun ShellCommandConfigurationContent(
onExecutionModeChanged: (ShellExecutionMode) -> Unit,
onTimeoutChanged: (Int) -> Unit,
onTestClick: () -> Unit,
- onSetupProModeClick: () -> Unit,
+ onSetupExpertModeClick: () -> Unit,
) {
val keyboardController = LocalSoftwareKeyboardController.current
Column(
@@ -350,18 +327,18 @@ private fun ShellCommandConfigurationContent(
)
if (state.executionMode == ShellExecutionMode.ADB &&
- state.proModeStatus != ProModeStatus.ENABLED
+ state.expertModeStatus != ExpertModeStatus.ENABLED
) {
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
- onClick = onSetupProModeClick,
- enabled = state.proModeStatus != ProModeStatus.UNSUPPORTED,
+ onClick = onSetupExpertModeClick,
+ enabled = state.expertModeStatus != ExpertModeStatus.UNSUPPORTED,
) {
Text(
- if (state.proModeStatus == ProModeStatus.UNSUPPORTED) {
- stringResource(R.string.action_shell_command_setup_pro_mode_unsupported)
+ if (state.expertModeStatus == ExpertModeStatus.UNSUPPORTED) {
+ stringResource(R.string.action_shell_command_setup_expert_mode_unsupported)
} else {
- stringResource(R.string.action_shell_command_setup_pro_mode)
+ stringResource(R.string.action_shell_command_setup_expert_mode)
},
)
}
@@ -378,7 +355,7 @@ private fun ShellCommandConfigurationContent(
state.executionMode != ShellExecutionMode.ADB ||
(
state.executionMode == ShellExecutionMode.ADB &&
- state.proModeStatus == ProModeStatus.ENABLED
+ state.expertModeStatus == ExpertModeStatus.ENABLED
)
),
) {
@@ -598,14 +575,14 @@ private fun PreviewShellCommandActionScreenTesting() {
@Preview
@Composable
-private fun PreviewShellCommandActionScreenProModeUnsupported() {
+private fun PreviewShellCommandActionScreenExpertModeUnsupported() {
KeyMapperTheme {
ShellCommandActionScreen(
state = ShellCommandActionState(
description = "ADB command example",
command = "echo 'Hello from ADB'",
executionMode = ShellExecutionMode.ADB,
- proModeStatus = ProModeStatus.UNSUPPORTED,
+ expertModeStatus = ExpertModeStatus.UNSUPPORTED,
),
)
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt
index b890e9ac19..1bca36c182 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionBottomSheet.kt
@@ -16,6 +16,7 @@ import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.OfflineBolt
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Keyboard
import androidx.compose.material.icons.rounded.Remove
@@ -44,15 +45,13 @@ import androidx.compose.ui.unit.dp
import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.compose.KeyMapperTheme
import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette
-import io.github.sds100.keymapper.base.utils.ProModeStatus
+import io.github.sds100.keymapper.base.utils.ExpertModeStatus
import io.github.sds100.keymapper.base.utils.ui.compose.AccessibilityServiceRequirementRow
import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText
+import io.github.sds100.keymapper.base.utils.ui.compose.ExpertModeRequirementRow
import io.github.sds100.keymapper.base.utils.ui.compose.HeaderText
import io.github.sds100.keymapper.base.utils.ui.compose.InputMethodRequirementRow
-import io.github.sds100.keymapper.base.utils.ui.compose.ProModeRequirementRow
import io.github.sds100.keymapper.base.utils.ui.compose.filledTonalButtonColorsError
-import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons
-import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -62,9 +61,9 @@ fun FixKeyEventActionBottomSheet(
sheetState: SheetState,
onDismissRequest: () -> Unit = {},
onSelectInputMethod: () -> Unit = {},
- onSelectProMode: () -> Unit = {},
+ onSelectExpertMode: () -> Unit = {},
onEnableAccessibilityServiceClick: () -> Unit = {},
- onEnableProModeClick: () -> Unit = {},
+ onEnableExpertModeClick: () -> Unit = {},
onEnableInputMethodClick: () -> Unit = {},
onChooseInputMethodClick: () -> Unit = {},
onDoneClick: () -> Unit = {},
@@ -135,18 +134,18 @@ fun FixKeyEventActionBottomSheet(
)
}
- val isProModeUnsupported = state.proModeStatus == ProModeStatus.UNSUPPORTED
+ val isExpertModeUnsupported = state.expertModeStatus == ExpertModeStatus.UNSUPPORTED
FixKeyEventActionOptionCard(
- onClick = onSelectProMode,
- selected = state is FixKeyEventActionState.ProMode,
- title = stringResource(R.string.pro_mode_app_bar_title),
- icon = KeyMapperIcons.ProModeIcon,
- enabled = !isProModeUnsupported,
+ onClick = onSelectExpertMode,
+ selected = state is FixKeyEventActionState.ExpertMode,
+ title = stringResource(R.string.expert_mode_app_bar_title),
+ icon = Icons.Outlined.OfflineBolt,
+ enabled = !isExpertModeUnsupported,
) {
- if (isProModeUnsupported) {
+ if (isExpertModeUnsupported) {
Text(
- stringResource(R.string.trigger_setup_pro_mode_unsupported),
+ stringResource(R.string.trigger_setup_expert_mode_unsupported),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
)
@@ -154,11 +153,11 @@ fun FixKeyEventActionBottomSheet(
val annotatedText = buildAnnotatedString {
appendInlineContent("icon", "[icon]")
append(" ")
- append(stringResource(R.string.fix_key_event_action_pro_mode_text_1))
+ append(stringResource(R.string.fix_key_event_action_expert_mode_text_1))
appendLine()
appendInlineContent("icon", "[icon]")
append(" ")
- append(stringResource(R.string.fix_key_event_action_pro_mode_text_2))
+ append(stringResource(R.string.fix_key_event_action_expert_mode_text_2))
}
val inlineContent = mapOf(
Pair(
@@ -224,13 +223,13 @@ fun FixKeyEventActionBottomSheet(
)
}
- is FixKeyEventActionState.ProMode -> {
- ProModeRequirementRow(
+ is FixKeyEventActionState.ExpertMode -> {
+ ExpertModeRequirementRow(
modifier = Modifier.fillMaxWidth(),
isVisible = true,
- proModeStatus = state.proModeStatus,
+ expertModeStatus = state.expertModeStatus,
buttonColors = ButtonDefaults.filledTonalButtonColorsError(),
- onClick = onEnableProModeClick,
+ onClick = onEnableExpertModeClick,
)
}
}
@@ -317,7 +316,7 @@ private fun InputMethodPreview() {
enablingRequiresUserInput = true,
isAccessibilityServiceEnabled = true,
isAutoSwitchImeEnabled = true,
- proModeStatus = ProModeStatus.ENABLED,
+ expertModeStatus = ExpertModeStatus.ENABLED,
),
)
}
@@ -326,7 +325,7 @@ private fun InputMethodPreview() {
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
-private fun ProModePreview() {
+private fun ExpertModePreview() {
KeyMapperTheme {
val sheetState = SheetState(
skipPartiallyExpanded = true,
@@ -336,8 +335,8 @@ private fun ProModePreview() {
FixKeyEventActionBottomSheet(
sheetState = sheetState,
- state = FixKeyEventActionState.ProMode(
- proModeStatus = ProModeStatus.DISABLED,
+ state = FixKeyEventActionState.ExpertMode(
+ expertModeStatus = ExpertModeStatus.DISABLED,
isAccessibilityServiceEnabled = true,
),
)
@@ -347,7 +346,7 @@ private fun ProModePreview() {
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
-private fun ProModeUnsupportedPreview() {
+private fun ExpertModeUnsupportedPreview() {
KeyMapperTheme {
val sheetState = SheetState(
skipPartiallyExpanded = true,
@@ -358,7 +357,7 @@ private fun ProModeUnsupportedPreview() {
FixKeyEventActionBottomSheet(
sheetState = sheetState,
state = FixKeyEventActionState.InputMethod(
- proModeStatus = ProModeStatus.UNSUPPORTED,
+ expertModeStatus = ExpertModeStatus.UNSUPPORTED,
isEnabled = false,
isChosen = false,
enablingRequiresUserInput = true,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionDelegate.kt
index 48a8a16c5c..61f8cbe818 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionDelegate.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionDelegate.kt
@@ -5,7 +5,7 @@ import dagger.hilt.android.scopes.ViewModelScoped
import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegate
import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCase
import io.github.sds100.keymapper.base.trigger.SetupInputMethodUseCase
-import io.github.sds100.keymapper.base.utils.ProModeStatus
+import io.github.sds100.keymapper.base.utils.ExpertModeStatus
import io.github.sds100.keymapper.base.utils.navigation.NavDestination
import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
import io.github.sds100.keymapper.base.utils.navigation.navigate
@@ -52,19 +52,19 @@ class FixKeyEventActionDelegateImpl @Inject constructor(
DialogProvider by dialogProvider,
NavigationProvider by navigationProvider {
- private val proModeStatus: Flow =
+ private val expertModeStatus: Flow =
if (Build.VERSION.SDK_INT >= Constants.SYSTEM_BRIDGE_MIN_API) {
systemBridgeConnectionManager.connectionState.map { state ->
when (state) {
- is SystemBridgeConnectionState.Connected -> ProModeStatus.ENABLED
- is SystemBridgeConnectionState.Disconnected -> ProModeStatus.DISABLED
+ is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED
+ is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED
}
}
} else {
- flowOf(ProModeStatus.UNSUPPORTED)
+ flowOf(ExpertModeStatus.UNSUPPORTED)
}
- private val isProModeSelected: Flow =
+ private val isExpertModeSelected: Flow =
preferenceRepository.get(Keys.keyEventActionsUseSystemBridge)
.map { it ?: PreferenceDefaults.KEY_EVENT_ACTIONS_USE_SYSTEM_BRIDGE }
@@ -82,26 +82,26 @@ class FixKeyEventActionDelegateImpl @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
private fun buildStateFlow(): Flow {
- return isProModeSelected.flatMapLatest { isProModeSelected ->
- if (isProModeSelected) {
+ return isExpertModeSelected.flatMapLatest { isExpertModeSelected ->
+ if (isExpertModeSelected) {
combine(
- proModeStatus,
+ expertModeStatus,
controlAccessibilityServiceUseCase.serviceState,
- ) { proModeStatus, serviceState ->
- FixKeyEventActionState.ProMode(
- proModeStatus = proModeStatus,
+ ) { expertModeStatus, serviceState ->
+ FixKeyEventActionState.ExpertMode(
+ expertModeStatus = expertModeStatus,
isAccessibilityServiceEnabled =
serviceState == AccessibilityServiceState.ENABLED,
)
}
} else {
combine(
- proModeStatus,
+ expertModeStatus,
setupInputMethodUseCase.isEnabled,
setupInputMethodUseCase.isChosen,
controlAccessibilityServiceUseCase.serviceState,
preferenceRepository.get(Keys.changeImeOnInputFocus),
- ) { proModeStatus, isEnabled, isChosen, serviceState, changeImeOnInputFocus ->
+ ) { expertModeStatus, isEnabled, isChosen, serviceState, changeImeOnInputFocus ->
val enablingRequiresUserInput =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
@@ -111,7 +111,7 @@ class FixKeyEventActionDelegateImpl @Inject constructor(
enablingRequiresUserInput = enablingRequiresUserInput,
isAccessibilityServiceEnabled =
serviceState == AccessibilityServiceState.ENABLED,
- proModeStatus = proModeStatus,
+ expertModeStatus = expertModeStatus,
isAutoSwitchImeEnabled = changeImeOnInputFocus
?: PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS,
)
@@ -134,9 +134,9 @@ class FixKeyEventActionDelegateImpl @Inject constructor(
}
}
- override fun onEnableProModeForKeyEventActionsClick() {
+ override fun onEnableExpertModeForKeyEventActionsClick() {
viewModelScope.launch {
- navigate("fix_key_event_action_pro_mode", NavDestination.ProMode)
+ navigate("fix_key_event_action_expert_mode", NavDestination.ExpertMode)
}
}
@@ -154,7 +154,7 @@ class FixKeyEventActionDelegateImpl @Inject constructor(
}
}
- override fun onSelectProMode() {
+ override fun onSelectExpertMode() {
preferenceRepository.set(Keys.keyEventActionsUseSystemBridge, true)
}
@@ -175,10 +175,10 @@ interface FixKeyEventActionDelegate {
fun showFixKeyEventActionBottomSheet()
fun dismissFixKeyEventActionBottomSheet()
fun onEnableAccessibilityServiceClick()
- fun onEnableProModeForKeyEventActionsClick()
+ fun onEnableExpertModeForKeyEventActionsClick()
fun onEnableImeClick()
fun onChooseImeClick()
- fun onSelectProMode()
+ fun onSelectExpertMode()
fun onSelectInputMethod()
fun onAutoSwitchImeCheckedChange(checked: Boolean)
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionState.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionState.kt
index 5bacfc2db5..370dd54e1a 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionState.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/FixKeyEventActionState.kt
@@ -1,10 +1,10 @@
package io.github.sds100.keymapper.base.actions.keyevent
-import io.github.sds100.keymapper.base.utils.ProModeStatus
+import io.github.sds100.keymapper.base.utils.ExpertModeStatus
sealed class FixKeyEventActionState {
abstract val isAccessibilityServiceEnabled: Boolean
- abstract val proModeStatus: ProModeStatus
+ abstract val expertModeStatus: ExpertModeStatus
data class InputMethod(
val isEnabled: Boolean,
@@ -16,11 +16,11 @@ sealed class FixKeyEventActionState {
val enablingRequiresUserInput: Boolean,
val isAutoSwitchImeEnabled: Boolean,
override val isAccessibilityServiceEnabled: Boolean,
- override val proModeStatus: ProModeStatus,
+ override val expertModeStatus: ExpertModeStatus,
) : FixKeyEventActionState()
- data class ProMode(
+ data class ExpertMode(
override val isAccessibilityServiceEnabled: Boolean,
- override val proModeStatus: ProModeStatus,
+ override val expertModeStatus: ExpertModeStatus,
) : FixKeyEventActionState()
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeTheme.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeTheme.kt
index 23ca891235..ddf61c8344 100755
--- a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeTheme.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeTheme.kt
@@ -103,6 +103,12 @@ fun KeyMapperTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composa
val customColorsPalette =
if (darkTheme) ComposeCustomColors.DarkPalette else ComposeCustomColors.LightPalette
+ SetSystemChrome(
+ statusBarColor = colorScheme.surfaceContainer,
+ navigationBarColor = colorScheme.surfaceContainer,
+ isDarkTheme = darkTheme,
+ )
+
CompositionLocalProvider(
LocalCustomColorsPalette provides customColorsPalette,
) {
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/SystemChrome.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/SystemChrome.kt
new file mode 100644
index 0000000000..59e8901869
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/compose/SystemChrome.kt
@@ -0,0 +1,47 @@
+package io.github.sds100.keymapper.base.compose
+
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+
+@Composable
+fun SetSystemChrome(statusBarColor: Color, navigationBarColor: Color, isDarkTheme: Boolean) {
+ val context = LocalContext.current
+ val view = LocalView.current
+
+ if (!view.isInEditMode) {
+ // Use keys to ensure it updates when theme changes.
+ // LaunchedEffect runs when keys change, and on initial composition.
+ // It also runs when restoring back stack if the composable is recreated.
+ LaunchedEffect(isDarkTheme, statusBarColor, navigationBarColor) {
+ val activity = context.findActivity() ?: return@LaunchedEffect
+
+ activity.enableEdgeToEdge(
+ statusBarStyle = if (isDarkTheme) {
+ SystemBarStyle.dark(statusBarColor.toArgb())
+ } else {
+ SystemBarStyle.light(statusBarColor.toArgb(), statusBarColor.toArgb())
+ },
+ navigationBarStyle = if (isDarkTheme) {
+ SystemBarStyle.dark(navigationBarColor.toArgb())
+ } else {
+ SystemBarStyle.light(navigationBarColor.toArgb(), navigationBarColor.toArgb())
+ },
+ )
+ }
+ }
+}
+
+private tailrec fun Context.findActivity(): ComponentActivity? = when (this) {
+ is ComponentActivity -> this
+ is ContextWrapper -> baseContext.findActivity()
+ else -> null
+}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt
index d3a956cd9c..857f30330c 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt
@@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.constraints
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
@@ -15,25 +14,19 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Android
-import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material.icons.rounded.Bluetooth
+import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -48,25 +41,28 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.compose.KeyMapperTheme
import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo
+import io.github.sds100.keymapper.base.utils.ui.compose.SearchAppBarActions
import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemFixedHeight
+import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemGroup
+import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemHeader
import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemModel
import io.github.sds100.keymapper.common.utils.State
import kotlinx.coroutines.flow.update
@Composable
fun ChooseConstraintScreen(modifier: Modifier = Modifier, viewModel: ChooseConstraintViewModel) {
- val listItems by viewModel.listItems.collectAsStateWithLifecycle()
+ val state by viewModel.groups.collectAsStateWithLifecycle()
val query by viewModel.searchQuery.collectAsStateWithLifecycle()
TimeConstraintBottomSheet(viewModel)
ChooseConstraintScreen(
modifier = modifier,
- state = listItems,
+ state = state,
query = query,
onQueryChange = { newQuery -> viewModel.searchQuery.update { newQuery } },
onCloseSearch = { viewModel.searchQuery.update { null } },
- onClickAction = viewModel::onListItemClick,
+ onClickConstraint = viewModel::onListItemClick,
onNavigateBack = viewModel::onNavigateBack,
)
}
@@ -75,78 +71,30 @@ fun ChooseConstraintScreen(modifier: Modifier = Modifier, viewModel: ChooseConst
@Composable
private fun ChooseConstraintScreen(
modifier: Modifier = Modifier,
- state: State>,
+ state: State>,
query: String? = null,
onQueryChange: (String) -> Unit = {},
onCloseSearch: () -> Unit = {},
- onClickAction: (String) -> Unit = {},
+ onClickConstraint: (String) -> Unit = {},
onNavigateBack: () -> Unit = {},
) {
- var isExpanded: Boolean by rememberSaveable { mutableStateOf(false) }
-
Scaffold(
modifier = modifier.displayCutoutPadding(),
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.choose_constraint_title)) },
+ )
+ },
bottomBar = {
BottomAppBar(
modifier = Modifier.imePadding(),
actions = {
- IconButton(onClick = {
- if (isExpanded) {
- onCloseSearch()
- isExpanded = false
- } else {
- onNavigateBack()
- }
- }) {
- Icon(
- Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = stringResource(
- R.string.bottom_app_bar_back_content_description,
- ),
- )
- }
-
- DockedSearchBar(
- modifier = Modifier.align(Alignment.CenterVertically),
- inputField = {
- SearchBarDefaults.InputField(
- modifier = Modifier.align(Alignment.CenterVertically),
- onSearch = {
- onQueryChange(it)
- isExpanded = false
- },
- leadingIcon = {
- Icon(
- Icons.Rounded.Search,
- contentDescription = null,
- )
- },
- enabled = state is State.Data,
- placeholder = { Text(stringResource(R.string.search_placeholder)) },
- query = query ?: "",
- onQueryChange = onQueryChange,
- expanded = isExpanded,
- onExpandedChange = { expanded ->
- if (expanded) {
- isExpanded = true
- } else {
- onCloseSearch()
- isExpanded = false
- }
- },
- )
- },
- // This is false to prevent an empty "content" showing underneath.
- expanded = isExpanded,
- onExpandedChange = { expanded ->
- if (expanded) {
- isExpanded = true
- } else {
- onCloseSearch()
- isExpanded = false
- }
- },
- content = {},
+ SearchAppBarActions(
+ onCloseSearch = onCloseSearch,
+ onNavigateBack = onNavigateBack,
+ onQueryChange = onQueryChange,
+ enabled = state is State.Data,
+ query = query,
)
},
)
@@ -167,33 +115,20 @@ private fun ChooseConstraintScreen(
),
) {
- Column {
- Text(
- modifier = Modifier.padding(
- start = 16.dp,
- end = 16.dp,
- top = 16.dp,
- bottom = 8.dp,
- ),
- text = stringResource(R.string.choose_constraint_title),
- style = MaterialTheme.typography.titleLarge,
- )
-
- when (state) {
- State.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize())
+ when (state) {
+ State.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize())
- is State.Data -> {
- if (state.data.isEmpty()) {
- EmptyScreen(
- modifier = Modifier.fillMaxSize(),
- )
- } else {
- ListScreen(
- modifier = Modifier.fillMaxSize(),
- listItems = state.data,
- onClickAction = onClickAction,
- )
- }
+ is State.Data -> {
+ if (state.data.isEmpty()) {
+ EmptyScreen(
+ modifier = Modifier.fillMaxSize(),
+ )
+ } else {
+ ListScreen(
+ modifier = Modifier.fillMaxSize(),
+ groups = state.data,
+ onClickConstraint = onClickConstraint,
+ )
}
}
}
@@ -233,8 +168,8 @@ private fun EmptyScreen(modifier: Modifier = Modifier) {
@Composable
private fun ListScreen(
modifier: Modifier = Modifier,
- listItems: List,
- onClickAction: (String) -> Unit,
+ groups: List,
+ onClickConstraint: (String) -> Unit,
) {
LazyVerticalGrid(
modifier = modifier,
@@ -243,12 +178,21 @@ private fun ListScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
- items(listItems, key = { it.id }) { model ->
- SimpleListItemFixedHeight(
- modifier = Modifier.fillMaxWidth(),
- model = model,
- onClick = { onClickAction(model.id) },
- )
+ for (group in groups) {
+ stickyHeader(contentType = "header") {
+ SimpleListItemHeader(modifier = Modifier.fillMaxWidth(), text = group.header)
+ }
+
+ items(
+ group.items,
+ contentType = { "list_item" },
+ ) { model ->
+ SimpleListItemFixedHeight(
+ modifier = Modifier.fillMaxWidth(),
+ model = model,
+ onClick = { onClickConstraint(model.id) },
+ )
+ }
}
}
}
@@ -261,18 +205,30 @@ private fun PreviewList() {
query = "Search query",
state = State.Data(
listOf(
- SimpleListItemModel(
- "app",
- title = "App in foreground",
- icon = ComposeIconInfo.Vector(Icons.Rounded.Android),
+ SimpleListItemGroup(
+ header = "Apps",
+ items = listOf(
+ SimpleListItemModel(
+ "app1",
+ title = "App in foreground",
+ icon = ComposeIconInfo.Vector(Icons.Rounded.Android),
+ ),
+ SimpleListItemModel(
+ "app2",
+ title = "App not in foreground",
+ icon = ComposeIconInfo.Vector(Icons.Rounded.Android),
+ ),
+ ),
),
- SimpleListItemModel(
- "app",
- title = "App not in foreground",
- icon = ComposeIconInfo.Vector(Icons.Rounded.Android),
- subtitle = "Error",
- isSubtitleError = true,
- isEnabled = false,
+ SimpleListItemGroup(
+ header = "Bluetooth",
+ items = listOf(
+ SimpleListItemModel(
+ "bt1",
+ title = "Bluetooth device connected",
+ icon = ComposeIconInfo.Vector(Icons.Rounded.Bluetooth),
+ ),
+ ),
),
),
),
@@ -285,21 +241,42 @@ private fun PreviewList() {
private fun PreviewGrid() {
KeyMapperTheme {
ChooseConstraintScreen(
- query = "Search query",
state = State.Data(
listOf(
- SimpleListItemModel(
- "app1",
- title = "App in foreground",
- icon = ComposeIconInfo.Vector(Icons.Rounded.Android),
+ SimpleListItemGroup(
+ header = "Apps",
+ items = listOf(
+ SimpleListItemModel(
+ "app1",
+ title = "App in foreground",
+ icon = ComposeIconInfo.Vector(Icons.Rounded.Android),
+ ),
+ SimpleListItemModel(
+ "app2",
+ title = "App not in foreground",
+ icon = ComposeIconInfo.Vector(Icons.Rounded.Android),
+ ),
+ ),
),
- SimpleListItemModel(
- "app2",
- title = "App not in foreground",
- icon = ComposeIconInfo.Vector(Icons.Rounded.Android),
- subtitle = "Error",
- isSubtitleError = true,
- isEnabled = false,
+ SimpleListItemGroup(
+ header = "WiFi",
+ items = listOf(
+ SimpleListItemModel(
+ "wifi1",
+ title = "WiFi is on",
+ icon = ComposeIconInfo.Vector(Icons.Rounded.Wifi),
+ subtitle = "Requires root",
+ isSubtitleError = true,
+ ),
+ SimpleListItemModel(
+ "wifi2",
+ title = "WiFi is off",
+ icon = ComposeIconInfo.Vector(Icons.Rounded.Wifi),
+ subtitle = "Requires root",
+ isSubtitleError = true,
+ isEnabled = false,
+ ),
+ ),
),
),
),
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt
index c39dacae37..836834e723 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt
@@ -1,5 +1,7 @@
package io.github.sds100.keymapper.base.constraints
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.StayCurrentPortrait
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -15,9 +17,12 @@ import io.github.sds100.keymapper.base.utils.navigation.navigate
import io.github.sds100.keymapper.base.utils.ui.DialogModel
import io.github.sds100.keymapper.base.utils.ui.DialogProvider
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
+import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo
+import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemGroup
import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemModel
import io.github.sds100.keymapper.base.utils.ui.showDialog
import io.github.sds100.keymapper.common.utils.Orientation
+import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.common.utils.State
import io.github.sds100.keymapper.system.camera.CameraLens
import javax.inject.Inject
@@ -45,70 +50,46 @@ class ChooseConstraintViewModel @Inject constructor(
NavigationProvider by navigationProvider {
companion object {
- private val ALL_CONSTRAINTS_ORDERED: Array = arrayOf(
- ConstraintId.APP_IN_FOREGROUND,
- ConstraintId.APP_NOT_IN_FOREGROUND,
- ConstraintId.APP_PLAYING_MEDIA,
- ConstraintId.APP_NOT_PLAYING_MEDIA,
- ConstraintId.MEDIA_PLAYING,
- ConstraintId.MEDIA_NOT_PLAYING,
-
- ConstraintId.BT_DEVICE_CONNECTED,
- ConstraintId.BT_DEVICE_DISCONNECTED,
-
- ConstraintId.SCREEN_ON,
- ConstraintId.SCREEN_OFF,
-
- ConstraintId.ORIENTATION_PORTRAIT,
- ConstraintId.ORIENTATION_LANDSCAPE,
- ConstraintId.ORIENTATION_0,
- ConstraintId.ORIENTATION_90,
- ConstraintId.ORIENTATION_180,
- ConstraintId.ORIENTATION_270,
-
- ConstraintId.FLASHLIGHT_ON,
- ConstraintId.FLASHLIGHT_OFF,
-
- ConstraintId.WIFI_ON,
- ConstraintId.WIFI_OFF,
- ConstraintId.WIFI_CONNECTED,
- ConstraintId.WIFI_DISCONNECTED,
-
- ConstraintId.IME_CHOSEN,
- ConstraintId.IME_NOT_CHOSEN,
-
- ConstraintId.KEYBOARD_SHOWING,
- ConstraintId.KEYBOARD_NOT_SHOWING,
-
- ConstraintId.DEVICE_IS_LOCKED,
- ConstraintId.DEVICE_IS_UNLOCKED,
- ConstraintId.LOCK_SCREEN_SHOWING,
- ConstraintId.LOCK_SCREEN_NOT_SHOWING,
-
- ConstraintId.IN_PHONE_CALL,
- ConstraintId.NOT_IN_PHONE_CALL,
- ConstraintId.PHONE_RINGING,
-
- ConstraintId.CHARGING,
- ConstraintId.DISCHARGING,
-
- ConstraintId.HINGE_CLOSED,
- ConstraintId.HINGE_OPEN,
-
- ConstraintId.TIME,
+ // Synthetic IDs for consolidated orientation list items (not actual ConstraintIds)
+ private const val DISPLAY_ORIENTATION_LIST_ITEM_ID = "display_orientation"
+ private const val PHYSICAL_ORIENTATION_LIST_ITEM_ID = "physical_orientation"
+
+ private val CATEGORY_ORDER = arrayOf(
+ ConstraintCategory.APPS,
+ ConstraintCategory.MEDIA,
+ ConstraintCategory.BLUETOOTH,
+ ConstraintCategory.DISPLAY,
+ ConstraintCategory.FLASHLIGHT,
+ ConstraintCategory.WIFI,
+ ConstraintCategory.KEYBOARD,
+ ConstraintCategory.LOCK,
+ ConstraintCategory.PHONE,
+ ConstraintCategory.POWER,
+ ConstraintCategory.DEVICE,
+ ConstraintCategory.TIME,
)
}
private val returnResult = MutableSharedFlow()
- private val allListItems: List by lazy { buildListItems() }
+ private val allGroupedListItems: List by lazy { buildListGroups() }
val searchQuery = MutableStateFlow(null)
- val listItems: StateFlow>> =
+ val groups: StateFlow>> =
searchQuery.map { query ->
- val filteredItems = allListItems.filter { it.title.containsQuery(query) }
- State.Data(filteredItems)
+ val groups = allGroupedListItems.mapNotNull { group ->
+
+ val filteredItems = group.items.filter { it.title.containsQuery(query) }
+
+ if (filteredItems.isEmpty()) {
+ return@mapNotNull null
+ } else {
+ group.copy(items = filteredItems)
+ }
+ }
+
+ State.Data(groups)
}.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading)
var timeConstraintState: ConstraintData.Time? by mutableStateOf(null)
@@ -138,6 +119,18 @@ class ChooseConstraintViewModel @Inject constructor(
fun onListItemClick(id: String) {
viewModelScope.launch {
+ // Handle synthetic list item IDs for consolidated orientation constraints
+ when (id) {
+ DISPLAY_ORIENTATION_LIST_ITEM_ID -> {
+ onSelectDisplayOrientationConstraint()
+ return@launch
+ }
+ PHYSICAL_ORIENTATION_LIST_ITEM_ID -> {
+ onSelectPhysicalOrientationConstraint()
+ return@launch
+ }
+ }
+
when (val constraintType = ConstraintId.valueOf(id)) {
ConstraintId.APP_IN_FOREGROUND,
ConstraintId.APP_NOT_IN_FOREGROUND,
@@ -158,32 +151,60 @@ class ChooseConstraintViewModel @Inject constructor(
ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff)
- ConstraintId.ORIENTATION_PORTRAIT ->
+ ConstraintId.DISPLAY_ORIENTATION_PORTRAIT ->
returnResult.emit(ConstraintData.OrientationPortrait)
- ConstraintId.ORIENTATION_LANDSCAPE ->
+ ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE ->
returnResult.emit(ConstraintData.OrientationLandscape)
- ConstraintId.ORIENTATION_0 ->
+ ConstraintId.DISPLAY_ORIENTATION_0 ->
returnResult.emit(
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0),
)
- ConstraintId.ORIENTATION_90 ->
+ ConstraintId.DISPLAY_ORIENTATION_90 ->
returnResult.emit(
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90),
)
- ConstraintId.ORIENTATION_180 ->
+ ConstraintId.DISPLAY_ORIENTATION_180 ->
returnResult.emit(
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180),
)
- ConstraintId.ORIENTATION_270 ->
+ ConstraintId.DISPLAY_ORIENTATION_270 ->
returnResult.emit(
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270),
)
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT ->
+ returnResult.emit(
+ ConstraintData.PhysicalOrientation(
+ physicalOrientation = PhysicalOrientation.PORTRAIT,
+ ),
+ )
+
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE ->
+ returnResult.emit(
+ ConstraintData.PhysicalOrientation(
+ physicalOrientation = PhysicalOrientation.LANDSCAPE,
+ ),
+ )
+
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED ->
+ returnResult.emit(
+ ConstraintData.PhysicalOrientation(
+ physicalOrientation = PhysicalOrientation.PORTRAIT_INVERTED,
+ ),
+ )
+
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED ->
+ returnResult.emit(
+ ConstraintData.PhysicalOrientation(
+ physicalOrientation = PhysicalOrientation.LANDSCAPE_INVERTED,
+ ),
+ )
+
ConstraintId.FLASHLIGHT_ON -> {
val lens = chooseFlashlightLens() ?: return@launch
returnResult.emit(ConstraintData.FlashlightOn(lens = lens))
@@ -278,25 +299,154 @@ class ChooseConstraintViewModel @Inject constructor(
return cameraLens
}
- private fun buildListItems(): List = buildList {
- ALL_CONSTRAINTS_ORDERED.forEach { id ->
- val title = getString(ConstraintUtils.getTitleStringId(id))
- val icon = ConstraintUtils.getIcon(id)
- val error = useCase.isSupported(id)
-
- val listItem = SimpleListItemModel(
- id = id.toString(),
- title = title,
- icon = icon,
- subtitle = error?.getFullMessage(this@ChooseConstraintViewModel),
- isSubtitleError = true,
- isEnabled = error == null,
+ private suspend fun onSelectDisplayOrientationConstraint() {
+ val items = listOf(
+ ConstraintId.DISPLAY_ORIENTATION_PORTRAIT to
+ getString(R.string.constraint_choose_orientation_portrait),
+ ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE to
+ getString(R.string.constraint_choose_orientation_landscape),
+ ConstraintId.DISPLAY_ORIENTATION_0 to
+ getString(R.string.constraint_choose_orientation_0),
+ ConstraintId.DISPLAY_ORIENTATION_90 to
+ getString(R.string.constraint_choose_orientation_90),
+ ConstraintId.DISPLAY_ORIENTATION_180 to
+ getString(R.string.constraint_choose_orientation_180),
+ ConstraintId.DISPLAY_ORIENTATION_270 to
+ getString(R.string.constraint_choose_orientation_270),
+ )
+
+ val dialog = DialogModel.SingleChoice(items)
+ val selectedOrientation = showDialog("choose_display_orientation", dialog) ?: return
+
+ val constraintData = when (selectedOrientation) {
+ ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait
+ ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape
+ ConstraintId.DISPLAY_ORIENTATION_0 ->
+ ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0)
+ ConstraintId.DISPLAY_ORIENTATION_90 ->
+ ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90)
+ ConstraintId.DISPLAY_ORIENTATION_180 ->
+ ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180)
+ ConstraintId.DISPLAY_ORIENTATION_270 ->
+ ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270)
+ else -> return
+ }
+
+ returnResult.emit(constraintData)
+ }
+
+ private suspend fun onSelectPhysicalOrientationConstraint() {
+ val items = listOf(
+ PhysicalOrientation.PORTRAIT to
+ getString(R.string.constraint_choose_physical_orientation_portrait),
+ PhysicalOrientation.LANDSCAPE to
+ getString(R.string.constraint_choose_physical_orientation_landscape),
+ PhysicalOrientation.PORTRAIT_INVERTED to
+ getString(R.string.constraint_choose_physical_orientation_portrait_inverted),
+ PhysicalOrientation.LANDSCAPE_INVERTED to
+ getString(R.string.constraint_choose_physical_orientation_landscape_inverted),
+ )
+
+ val dialog = DialogModel.SingleChoice(items)
+ val selectedOrientation = showDialog("choose_physical_orientation", dialog) ?: return
+
+ returnResult.emit(
+ ConstraintData.PhysicalOrientation(physicalOrientation = selectedOrientation),
+ )
+ }
+
+ private fun buildListGroups(): List = buildList {
+ // Filter out individual orientation constraints - show only the consolidated ones
+ val filteredConstraints = ConstraintId.entries.filter { constraintId ->
+ constraintId !in listOf(
+ ConstraintId.DISPLAY_ORIENTATION_PORTRAIT,
+ ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE,
+ ConstraintId.DISPLAY_ORIENTATION_0,
+ ConstraintId.DISPLAY_ORIENTATION_90,
+ ConstraintId.DISPLAY_ORIENTATION_180,
+ ConstraintId.DISPLAY_ORIENTATION_270,
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT,
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE,
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,
)
+ }
- add(listItem)
+ val listItems = buildListItems(filteredConstraints)
+
+ // Add synthetic orientation list items
+ val displayOrientationItem = SimpleListItemModel(
+ id = DISPLAY_ORIENTATION_LIST_ITEM_ID,
+ title = getString(R.string.constraint_choose_screen_orientation),
+ icon = ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait),
+ isEnabled = true,
+ )
+
+ val physicalOrientationItem = SimpleListItemModel(
+ id = PHYSICAL_ORIENTATION_LIST_ITEM_ID,
+ title = getString(R.string.constraint_choose_physical_orientation),
+ icon = ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait),
+ isEnabled = true,
+ )
+
+ for (category in CATEGORY_ORDER) {
+ val header = getString(ConstraintUtils.getCategoryLabel(category))
+
+ val categoryItems = listItems.filter { item ->
+ item.isEnabled &&
+ try {
+ ConstraintUtils.getCategory(ConstraintId.valueOf(item.id)) == category
+ } catch (e: IllegalArgumentException) {
+ false
+ }
+ }.toMutableList()
+
+ // Add synthetic orientation items to DISPLAY category
+ if (category == ConstraintCategory.DISPLAY) {
+ categoryItems.add(displayOrientationItem)
+ categoryItems.add(physicalOrientationItem)
+ }
+
+ val group = SimpleListItemGroup(
+ header,
+ items = categoryItems,
+ )
+
+ if (group.items.isNotEmpty()) {
+ add(group)
+ }
+ }
+
+ val unsupportedItems = listItems.filter { !it.isEnabled }
+ if (unsupportedItems.isNotEmpty()) {
+ val unsupportedGroup = SimpleListItemGroup(
+ header = getString(R.string.choose_constraint_group_unsupported),
+ items = unsupportedItems,
+ )
+ add(unsupportedGroup)
}
}
+ private fun buildListItems(constraintIds: List): List =
+ buildList {
+ for (constraintId in constraintIds) {
+ val title = getString(ConstraintUtils.getTitleStringId(constraintId))
+ val icon = ConstraintUtils.getIcon(constraintId)
+ val error = useCase.isSupported(constraintId)
+
+ val listItem = SimpleListItemModel(
+ id = constraintId.toString(),
+ title = title,
+ icon = icon,
+ subtitle = error?.getFullMessage(this@ChooseConstraintViewModel),
+ isSubtitleError = true,
+ isEnabled = error == null,
+ )
+
+ add(listItem)
+ }
+ }
+
private suspend fun onSelectWifiConnectedConstraint(type: ConstraintId) {
val knownSSIDs: List = useCase.getKnownWiFiSSIDs()
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt
index 547802579f..a2aa471fbd 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt
@@ -1,6 +1,7 @@
package io.github.sds100.keymapper.base.constraints
import io.github.sds100.keymapper.common.utils.Orientation
+import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.common.utils.getKey
import io.github.sds100.keymapper.common.utils.valueOrNull
import io.github.sds100.keymapper.data.entities.ConstraintEntity
@@ -69,21 +70,37 @@ sealed class ConstraintData {
@Serializable
data object OrientationPortrait : ConstraintData() {
- override val id: ConstraintId = ConstraintId.ORIENTATION_PORTRAIT
+ override val id: ConstraintId = ConstraintId.DISPLAY_ORIENTATION_PORTRAIT
}
@Serializable
data object OrientationLandscape : ConstraintData() {
- override val id: ConstraintId = ConstraintId.ORIENTATION_LANDSCAPE
+ override val id: ConstraintId = ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE
}
@Serializable
data class OrientationCustom(val orientation: Orientation) : ConstraintData() {
override val id: ConstraintId = when (orientation) {
- Orientation.ORIENTATION_0 -> ConstraintId.ORIENTATION_0
- Orientation.ORIENTATION_90 -> ConstraintId.ORIENTATION_90
- Orientation.ORIENTATION_180 -> ConstraintId.ORIENTATION_180
- Orientation.ORIENTATION_270 -> ConstraintId.ORIENTATION_270
+ Orientation.ORIENTATION_0 -> ConstraintId.DISPLAY_ORIENTATION_0
+ Orientation.ORIENTATION_90 -> ConstraintId.DISPLAY_ORIENTATION_90
+ Orientation.ORIENTATION_180 -> ConstraintId.DISPLAY_ORIENTATION_180
+ Orientation.ORIENTATION_270 -> ConstraintId.DISPLAY_ORIENTATION_270
+ }
+ }
+
+ @Serializable
+ data class PhysicalOrientation(
+ val physicalOrientation: io.github.sds100.keymapper.common.utils.PhysicalOrientation,
+ ) : ConstraintData() {
+ override val id: ConstraintId = when (physicalOrientation) {
+ io.github.sds100.keymapper.common.utils.PhysicalOrientation.PORTRAIT ->
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT
+ io.github.sds100.keymapper.common.utils.PhysicalOrientation.LANDSCAPE ->
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE
+ io.github.sds100.keymapper.common.utils.PhysicalOrientation.PORTRAIT_INVERTED ->
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED
+ io.github.sds100.keymapper.common.utils.PhysicalOrientation.LANDSCAPE_INVERTED ->
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED
}
}
@@ -316,6 +333,15 @@ object ConstraintEntityMapper {
ConstraintEntity.ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait
ConstraintEntity.ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape
+ ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT ->
+ ConstraintData.PhysicalOrientation(PhysicalOrientation.PORTRAIT)
+ ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE ->
+ ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE)
+ ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED ->
+ ConstraintData.PhysicalOrientation(PhysicalOrientation.PORTRAIT_INVERTED)
+ ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED ->
+ ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE_INVERTED)
+
ConstraintEntity.SCREEN_OFF -> ConstraintData.ScreenOff
ConstraintEntity.SCREEN_ON -> ConstraintData.ScreenOn
@@ -499,6 +525,25 @@ object ConstraintEntityMapper {
ConstraintEntity.ORIENTATION_PORTRAIT,
)
+ is ConstraintData.PhysicalOrientation -> when (constraint.data.physicalOrientation) {
+ PhysicalOrientation.PORTRAIT -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT,
+ )
+ PhysicalOrientation.LANDSCAPE -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE,
+ )
+ PhysicalOrientation.PORTRAIT_INVERTED -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
+ )
+ PhysicalOrientation.LANDSCAPE_INVERTED -> ConstraintEntity(
+ uid = constraint.uid,
+ ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,
+ )
+ }
+
is ConstraintData.ScreenOff -> ConstraintEntity(
uid = constraint.uid,
ConstraintEntity.SCREEN_OFF,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintCategory.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintCategory.kt
new file mode 100644
index 0000000000..e4937d1529
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintCategory.kt
@@ -0,0 +1,16 @@
+package io.github.sds100.keymapper.base.constraints
+
+enum class ConstraintCategory {
+ APPS,
+ MEDIA,
+ BLUETOOTH,
+ DISPLAY,
+ FLASHLIGHT,
+ WIFI,
+ KEYBOARD,
+ LOCK,
+ PHONE,
+ POWER,
+ DEVICE,
+ TIME,
+}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt
index e8b3f784e9..4bbbcf1721 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt
@@ -7,6 +7,7 @@ enum class ConstraintDependency {
CONNECTED_BT_DEVICES,
SCREEN_STATE,
DISPLAY_ORIENTATION,
+ PHYSICAL_ORIENTATION,
FLASHLIGHT_STATE,
WIFI_SSID,
WIFI_STATE,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt
index b48b61b8c2..06463c6fd7 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt
@@ -19,12 +19,17 @@ enum class ConstraintId {
SCREEN_ON,
SCREEN_OFF,
- ORIENTATION_PORTRAIT,
- ORIENTATION_LANDSCAPE,
- ORIENTATION_0,
- ORIENTATION_90,
- ORIENTATION_180,
- ORIENTATION_270,
+ DISPLAY_ORIENTATION_PORTRAIT,
+ DISPLAY_ORIENTATION_LANDSCAPE,
+ DISPLAY_ORIENTATION_0,
+ DISPLAY_ORIENTATION_90,
+ DISPLAY_ORIENTATION_180,
+ DISPLAY_ORIENTATION_270,
+
+ PHYSICAL_ORIENTATION_PORTRAIT,
+ PHYSICAL_ORIENTATION_LANDSCAPE,
+ PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
+ PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,
FLASHLIGHT_ON,
FLASHLIGHT_OFF,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt
index 629a610c3a..00ac3781ee 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt
@@ -4,6 +4,7 @@ import android.media.AudioManager
import android.os.Build
import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService
import io.github.sds100.keymapper.common.utils.Orientation
+import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.common.utils.firstBlocking
import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo
import io.github.sds100.keymapper.system.camera.CameraAdapter
@@ -43,6 +44,9 @@ class LazyConstraintSnapshot(
devicesAdapter.connectedBluetoothDevices.value
}
private val orientation: Orientation by lazy { displayAdapter.cachedOrientation }
+ private val physicalOrientation: PhysicalOrientation by lazy {
+ displayAdapter.cachedPhysicalOrientation
+ }
private val isScreenOn: Boolean by lazy { displayAdapter.isScreenOn.firstBlocking() }
private val appsPlayingMedia: List by lazy {
mediaAdapter.getActiveMediaSessionPackages()
@@ -117,6 +121,9 @@ class LazyConstraintSnapshot(
orientation == Orientation.ORIENTATION_0 ||
orientation == Orientation.ORIENTATION_180
+ is ConstraintData.PhysicalOrientation ->
+ physicalOrientation == constraint.data.physicalOrientation
+
is ConstraintData.ScreenOff -> !isScreenOn
is ConstraintData.ScreenOn -> isScreenOn
is ConstraintData.FlashlightOff -> !cameraAdapter.isFlashlightOn(constraint.data.lens)
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt
index 62246d6817..94b02aebc9 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt
@@ -6,6 +6,7 @@ import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo
import io.github.sds100.keymapper.common.utils.Orientation
+import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.common.utils.TimeUtils
import io.github.sds100.keymapper.common.utils.handle
import io.github.sds100.keymapper.common.utils.valueIfFailure
@@ -82,6 +83,21 @@ class ConstraintUiHelper(
is ConstraintData.OrientationPortrait ->
getString(R.string.constraint_choose_orientation_portrait)
+ is ConstraintData.PhysicalOrientation -> {
+ val resId = when (constraint.data.physicalOrientation) {
+ PhysicalOrientation.PORTRAIT ->
+ R.string.constraint_choose_physical_orientation_portrait
+ PhysicalOrientation.LANDSCAPE ->
+ R.string.constraint_choose_physical_orientation_landscape
+ PhysicalOrientation.PORTRAIT_INVERTED ->
+ R.string.constraint_choose_physical_orientation_portrait_inverted
+ PhysicalOrientation.LANDSCAPE_INVERTED ->
+ R.string.constraint_choose_physical_orientation_landscape_inverted
+ }
+
+ getString(resId)
+ }
+
is ConstraintData.ScreenOff ->
getString(R.string.constraint_screen_off_description)
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt
index 2d7b1912aa..80cd56c1c7 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt
@@ -1,5 +1,6 @@
package io.github.sds100.keymapper.base.constraints
+import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Battery2Bar
import androidx.compose.material.icons.outlined.BatteryChargingFull
@@ -30,6 +31,89 @@ import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo
object ConstraintUtils {
+ @StringRes
+ fun getCategoryLabel(category: ConstraintCategory): Int = when (category) {
+ ConstraintCategory.APPS -> R.string.constraint_cat_apps
+ ConstraintCategory.MEDIA -> R.string.constraint_cat_media
+ ConstraintCategory.BLUETOOTH -> R.string.constraint_cat_bluetooth
+ ConstraintCategory.DISPLAY -> R.string.constraint_cat_display
+ ConstraintCategory.FLASHLIGHT -> R.string.constraint_cat_flashlight
+ ConstraintCategory.WIFI -> R.string.constraint_cat_wifi
+ ConstraintCategory.KEYBOARD -> R.string.constraint_cat_keyboard
+ ConstraintCategory.LOCK -> R.string.constraint_cat_lock
+ ConstraintCategory.PHONE -> R.string.constraint_cat_phone
+ ConstraintCategory.POWER -> R.string.constraint_cat_power
+ ConstraintCategory.DEVICE -> R.string.constraint_cat_device
+ ConstraintCategory.TIME -> R.string.constraint_cat_time
+ }
+
+ fun getCategory(constraintId: ConstraintId): ConstraintCategory = when (constraintId) {
+ ConstraintId.APP_IN_FOREGROUND,
+ ConstraintId.APP_NOT_IN_FOREGROUND,
+ ConstraintId.APP_PLAYING_MEDIA,
+ ConstraintId.APP_NOT_PLAYING_MEDIA,
+ -> ConstraintCategory.APPS
+
+ ConstraintId.MEDIA_PLAYING,
+ ConstraintId.MEDIA_NOT_PLAYING,
+ -> ConstraintCategory.MEDIA
+
+ ConstraintId.BT_DEVICE_CONNECTED,
+ ConstraintId.BT_DEVICE_DISCONNECTED,
+ -> ConstraintCategory.BLUETOOTH
+
+ ConstraintId.SCREEN_ON,
+ ConstraintId.SCREEN_OFF,
+ ConstraintId.DISPLAY_ORIENTATION_PORTRAIT,
+ ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE,
+ ConstraintId.DISPLAY_ORIENTATION_0,
+ ConstraintId.DISPLAY_ORIENTATION_90,
+ ConstraintId.DISPLAY_ORIENTATION_180,
+ ConstraintId.DISPLAY_ORIENTATION_270,
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT,
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE,
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,
+ -> ConstraintCategory.DISPLAY
+
+ ConstraintId.FLASHLIGHT_ON,
+ ConstraintId.FLASHLIGHT_OFF,
+ -> ConstraintCategory.FLASHLIGHT
+
+ ConstraintId.WIFI_ON,
+ ConstraintId.WIFI_OFF,
+ ConstraintId.WIFI_CONNECTED,
+ ConstraintId.WIFI_DISCONNECTED,
+ -> ConstraintCategory.WIFI
+
+ ConstraintId.IME_CHOSEN,
+ ConstraintId.IME_NOT_CHOSEN,
+ ConstraintId.KEYBOARD_SHOWING,
+ ConstraintId.KEYBOARD_NOT_SHOWING,
+ -> ConstraintCategory.KEYBOARD
+
+ ConstraintId.DEVICE_IS_LOCKED,
+ ConstraintId.DEVICE_IS_UNLOCKED,
+ ConstraintId.LOCK_SCREEN_SHOWING,
+ ConstraintId.LOCK_SCREEN_NOT_SHOWING,
+ -> ConstraintCategory.LOCK
+
+ ConstraintId.IN_PHONE_CALL,
+ ConstraintId.NOT_IN_PHONE_CALL,
+ ConstraintId.PHONE_RINGING,
+ -> ConstraintCategory.PHONE
+
+ ConstraintId.CHARGING,
+ ConstraintId.DISCHARGING,
+ -> ConstraintCategory.POWER
+
+ ConstraintId.HINGE_CLOSED,
+ ConstraintId.HINGE_OPEN,
+ -> ConstraintCategory.DEVICE
+
+ ConstraintId.TIME -> ConstraintCategory.TIME
+ }
+
fun getIcon(constraintId: ConstraintId): ComposeIconInfo = when (constraintId) {
ConstraintId.APP_IN_FOREGROUND,
ConstraintId.APP_NOT_IN_FOREGROUND,
@@ -47,21 +131,29 @@ object ConstraintUtils {
Icons.Outlined.BluetoothDisabled,
)
- ConstraintId.ORIENTATION_0,
- ConstraintId.ORIENTATION_180,
+ ConstraintId.DISPLAY_ORIENTATION_0,
+ ConstraintId.DISPLAY_ORIENTATION_180,
-> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait)
- ConstraintId.ORIENTATION_90,
- ConstraintId.ORIENTATION_270,
+ ConstraintId.DISPLAY_ORIENTATION_90,
+ ConstraintId.DISPLAY_ORIENTATION_270,
-> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape)
- ConstraintId.ORIENTATION_LANDSCAPE -> ComposeIconInfo.Vector(
+ ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ComposeIconInfo.Vector(
Icons.Outlined.StayCurrentLandscape,
)
- ConstraintId.ORIENTATION_PORTRAIT -> ComposeIconInfo.Vector(
+ ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ComposeIconInfo.Vector(
Icons.Outlined.StayCurrentPortrait,
)
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT,
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
+ -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait)
+
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE,
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,
+ -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape)
+
ConstraintId.SCREEN_OFF -> ComposeIconInfo.Vector(Icons.Outlined.MobileOff)
ConstraintId.SCREEN_ON -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait)
@@ -114,12 +206,21 @@ object ConstraintUtils {
R.string.constraint_choose_bluetooth_device_disconnected
ConstraintId.SCREEN_ON -> R.string.constraint_choose_screen_on_description
ConstraintId.SCREEN_OFF -> R.string.constraint_choose_screen_off_description
- ConstraintId.ORIENTATION_PORTRAIT -> R.string.constraint_choose_orientation_portrait
- ConstraintId.ORIENTATION_LANDSCAPE -> R.string.constraint_choose_orientation_landscape
- ConstraintId.ORIENTATION_0 -> R.string.constraint_choose_orientation_0
- ConstraintId.ORIENTATION_90 -> R.string.constraint_choose_orientation_90
- ConstraintId.ORIENTATION_180 -> R.string.constraint_choose_orientation_180
- ConstraintId.ORIENTATION_270 -> R.string.constraint_choose_orientation_270
+ ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> R.string.constraint_choose_orientation_portrait
+ ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE ->
+ R.string.constraint_choose_orientation_landscape
+ ConstraintId.DISPLAY_ORIENTATION_0 -> R.string.constraint_choose_orientation_0
+ ConstraintId.DISPLAY_ORIENTATION_90 -> R.string.constraint_choose_orientation_90
+ ConstraintId.DISPLAY_ORIENTATION_180 -> R.string.constraint_choose_orientation_180
+ ConstraintId.DISPLAY_ORIENTATION_270 -> R.string.constraint_choose_orientation_270
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT ->
+ R.string.constraint_choose_physical_orientation_portrait
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE ->
+ R.string.constraint_choose_physical_orientation_landscape
+ ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED ->
+ R.string.constraint_choose_physical_orientation_portrait_inverted
+ ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED ->
+ R.string.constraint_choose_physical_orientation_landscape_inverted
ConstraintId.FLASHLIGHT_ON -> R.string.constraint_flashlight_on
ConstraintId.FLASHLIGHT_OFF -> R.string.constraint_flashlight_off
ConstraintId.WIFI_ON -> R.string.constraint_wifi_on
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt
index 87e38e6af3..8bf75d6f24 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt
@@ -71,6 +71,8 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor(
ConstraintDependency.SCREEN_STATE -> displayAdapter.isScreenOn.map { dependency }
ConstraintDependency.DISPLAY_ORIENTATION ->
displayAdapter.orientation.map { dependency }
+ ConstraintDependency.PHYSICAL_ORIENTATION ->
+ displayAdapter.physicalOrientation.map { dependency }
ConstraintDependency.FLASHLIGHT_STATE -> merge(
cameraAdapter.isFlashlightOnFlow(CameraLens.FRONT),
cameraAdapter.isFlashlightOnFlow(CameraLens.BACK),
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 97de63d1bd..befa0d8122 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,7 +30,6 @@ 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.combine
@@ -49,17 +48,12 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor(
private val toastAdapter: ToastAdapter,
private val resourceProvider: ResourceProvider,
private val vibrator: VibratorAdapter,
- @Assisted
- private val coroutineScope: CoroutineScope,
private val inputEventHub: InputEventHub,
) : DetectKeyMapsUseCase {
@AssistedFactory
interface Factory {
- fun create(
- accessibilityService: IAccessibilityService,
- coroutineScope: CoroutineScope,
- ): DetectKeyMapsUseCaseImpl
+ fun create(accessibilityService: IAccessibilityService): DetectKeyMapsUseCaseImpl
}
companion object {
@@ -228,15 +222,15 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor(
}
}
- override fun imitateEvdevEvent(devicePath: String, type: Int, code: Int, value: Int) {
+ override fun imitateEvdevEvent(deviceId: Int, type: Int, code: Int, value: Int) {
if (inputEventHub.isSystemBridgeConnected()) {
Timber.d(
- "Imitate evdev event, device path: $devicePath, type: $type, code: $code, value: $value",
+ "Imitate evdev event, device id: $deviceId, type: $type, code: $code, value: $value",
)
- inputEventHub.injectEvdevEvent(devicePath, type, code, value)
+ inputEventHub.injectEvdevEvent(deviceId, type, code, value)
} else {
Timber.w(
- "Cannot imitate evdev event without system bridge connected. Device path: $devicePath, type: $type, code: $code, value: $value",
+ "Cannot imitate evdev event without system bridge connected.",
)
}
}
@@ -268,5 +262,5 @@ interface DetectKeyMapsUseCase {
source: Int = InputDevice.SOURCE_UNKNOWN,
)
- fun imitateEvdevEvent(devicePath: String, type: Int, code: Int, value: Int)
+ fun imitateEvdevEvent(deviceId: Int, type: Int, code: Int, value: Int)
}
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 7429079ace..b2b137abbe 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
@@ -6,6 +6,7 @@ import androidx.collection.keyIterator
import androidx.collection.valueIterator
import io.github.sds100.keymapper.base.actions.Action
import io.github.sds100.keymapper.base.actions.ActionData
+import io.github.sds100.keymapper.base.actions.PerformActionTriggerDevice
import io.github.sds100.keymapper.base.actions.PerformActionsUseCase
import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot
import io.github.sds100.keymapper.base.constraints.ConstraintState
@@ -97,7 +98,8 @@ class KeyMapAlgorithm(
*/
private var doublePressTimeoutTimes = longArrayOf()
- private var actionMap: SparseArrayCompat = SparseArrayCompat()
+ var actionMap: SparseArrayCompat = SparseArrayCompat()
+ private set
var triggers: Array = emptyArray()
private set
@@ -140,7 +142,8 @@ class KeyMapAlgorithm(
* The actions to perform when each trigger is detected. The order matches with
* [triggers].
*/
- private var triggerActions: Array = arrayOf()
+ var triggerActions: Array = arrayOf()
+ private set
/**
* Stores whether each event in each parallel trigger need to be released after being held down.
@@ -284,6 +287,7 @@ class KeyMapAlgorithm(
val triggerActions = mutableListOf()
val triggerConstraints = mutableListOf>()
+ val triggerPerformActionDevices = mutableListOf()
val sequenceTriggerActionPerformers =
mutableMapOf()
@@ -648,13 +652,8 @@ class KeyMapAlgorithm(
val event = EvdevEventAlgo(
keyCode = inputEvent.androidCode,
clickType = null,
- devicePath = inputEvent.device.path,
- device = EvdevDeviceInfo(
- name = inputEvent.device.name,
- bus = inputEvent.device.bus,
- vendor = inputEvent.device.vendor,
- product = inputEvent.device.product,
- ),
+ deviceId = inputEvent.deviceId,
+ device = inputEvent.deviceInfo,
scanCode = inputEvent.code,
)
@@ -932,6 +931,7 @@ class KeyMapAlgorithm(
performActionsAfterSequenceTriggerTimeout[triggerIndex] =
performActionsAfterSequenceTriggerTimeout(
+ event,
triggerIndex,
overlappingSequenceTrigger,
)
@@ -956,7 +956,7 @@ class KeyMapAlgorithm(
oldJob?.cancel()
parallelTriggerLongPressJobs.put(
triggerIndex,
- performActionsAfterLongPressDelay(triggerIndex),
+ performActionsAfterLongPressDelay(event, triggerIndex),
)
}
}
@@ -1020,6 +1020,7 @@ class KeyMapAlgorithm(
val trigger = triggers[triggerIndex]
parallelTriggerActionPerformers[triggerIndex]?.onTriggered(
+ device = event.performActionDevice(),
calledOnTriggerRelease = false,
metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions),
)
@@ -1394,6 +1395,7 @@ class KeyMapAlgorithm(
if (lastHeldDownEventIndex != triggers[triggerIndex].keys.lastIndex) {
parallelTriggerActionPerformers[triggerIndex]?.onReleased(
metaStateFromKeyEvent + metaStateFromActions,
+ device = event.performActionDevice(),
)
}
}
@@ -1425,6 +1427,7 @@ class KeyMapAlgorithm(
detectedSequenceTriggerIndexes.forEach { triggerIndex ->
sequenceTriggerActionPerformers[triggerIndex]?.onTriggered(
+ device = event.performActionDevice(),
metaState = metaStateFromActions.withFlag(
metaStateFromKeyEvent,
),
@@ -1433,6 +1436,7 @@ class KeyMapAlgorithm(
detectedParallelTriggerIndexes.forEach { triggerIndex ->
parallelTriggerActionPerformers[triggerIndex]?.onTriggered(
+ device = event.performActionDevice(),
calledOnTriggerRelease = true,
metaState = metaStateFromActions.withFlag(metaStateFromKeyEvent),
)
@@ -1490,14 +1494,14 @@ class KeyMapAlgorithm(
)
} else if (event is EvdevEventAlgo) {
useCase.imitateEvdevEvent(
- devicePath = event.devicePath,
+ deviceId = event.deviceId,
KMEvdevEvent.TYPE_KEY_EVENT,
event.scanCode,
KMEvdevEvent.VALUE_DOWN,
)
useCase.imitateEvdevEvent(
- devicePath = event.devicePath,
+ deviceId = event.deviceId,
KMEvdevEvent.TYPE_KEY_EVENT,
event.scanCode,
KMEvdevEvent.VALUE_UP,
@@ -1544,20 +1548,20 @@ class KeyMapAlgorithm(
} else if (event is EvdevEventAlgo) {
if (imitateUpKeyEvent) {
useCase.imitateEvdevEvent(
- devicePath = event.devicePath,
+ deviceId = event.deviceId,
type = KMEvdevEvent.TYPE_KEY_EVENT,
code = event.scanCode,
value = KMEvdevEvent.VALUE_UP,
)
} else {
useCase.imitateEvdevEvent(
- devicePath = event.devicePath,
+ deviceId = event.deviceId,
type = KMEvdevEvent.TYPE_KEY_EVENT,
code = event.scanCode,
value = KMEvdevEvent.VALUE_DOWN,
)
useCase.imitateEvdevEvent(
- devicePath = event.devicePath,
+ deviceId = event.deviceId,
type = KMEvdevEvent.TYPE_KEY_EVENT,
code = event.scanCode,
value = KMEvdevEvent.VALUE_UP,
@@ -1651,6 +1655,7 @@ class KeyMapAlgorithm(
detectedTriggerIndexes.forEach { triggerIndex ->
parallelTriggerActionPerformers[triggerIndex]?.onTriggered(
+ device = event.performActionDevice(),
calledOnTriggerRelease = true,
metaState = metaStateFromActions.withFlag(metaStateFromKeyEvent),
)
@@ -1715,30 +1720,33 @@ class KeyMapAlgorithm(
/**
* For parallel triggers only.
*/
- private fun performActionsAfterLongPressDelay(triggerIndex: Int) = coroutineScope.launch {
- delay(longPressDelay(triggers[triggerIndex]))
+ private fun performActionsAfterLongPressDelay(event: AlgoEvent, triggerIndex: Int) =
+ coroutineScope.launch {
+ delay(longPressDelay(triggers[triggerIndex]))
- parallelTriggerActionPerformers[triggerIndex]?.onTriggered(
- calledOnTriggerRelease = false,
- metaState = metaStateFromActions.withFlag(metaStateFromKeyEvent),
- )
+ parallelTriggerActionPerformers[triggerIndex]?.onTriggered(
+ device = event.performActionDevice(),
+ calledOnTriggerRelease = false,
+ metaState = metaStateFromActions.withFlag(metaStateFromKeyEvent),
+ )
- if (triggers[triggerIndex].vibrate ||
- forceVibrate.value ||
- triggers[triggerIndex].longPressDoubleVibration
- ) {
- useCase.vibrate(vibrateDuration(triggers[triggerIndex]))
- }
+ if (triggers[triggerIndex].vibrate ||
+ forceVibrate.value ||
+ triggers[triggerIndex].longPressDoubleVibration
+ ) {
+ useCase.vibrate(vibrateDuration(triggers[triggerIndex]))
+ }
- if (triggers[triggerIndex].showToast) {
- useCase.showTriggeredToast()
+ if (triggers[triggerIndex].showToast) {
+ useCase.showTriggeredToast()
+ }
}
- }
/**
* For parallel triggers only.
*/
private fun performActionsAfterSequenceTriggerTimeout(
+ event: AlgoEvent,
triggerIndex: Int,
sequenceTriggerIndex: Int,
) = coroutineScope.launch {
@@ -1753,6 +1761,7 @@ class KeyMapAlgorithm(
}
parallelTriggerActionPerformers[triggerIndex]?.onTriggered(
+ device = event.performActionDevice(),
calledOnTriggerRelease = true,
metaState = metaStateFromActions.withFlag(metaStateFromKeyEvent),
)
@@ -1944,7 +1953,7 @@ class KeyMapAlgorithm(
}
private data class EvdevEventAlgo(
- val devicePath: String,
+ val deviceId: Int,
val device: EvdevDeviceInfo,
val scanCode: Int,
val keyCode: Int,
@@ -1978,4 +1987,11 @@ class KeyMapAlgorithm(
) : AlgoEvent()
private data class TriggerKeyLocation(val triggerIndex: Int, val keyIndex: Int)
+
+ private fun AlgoEvent.performActionDevice(): PerformActionTriggerDevice {
+ return when (this) {
+ is EvdevEventAlgo -> PerformActionTriggerDevice.Evdev(deviceId)
+ else -> PerformActionTriggerDevice.Default
+ }
+ }
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt
index 955023690b..61296637b4 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt
@@ -1,5 +1,6 @@
package io.github.sds100.keymapper.base.detection
+import io.github.sds100.keymapper.base.actions.ActionData
import io.github.sds100.keymapper.base.actions.PerformActionsUseCase
import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase
import io.github.sds100.keymapper.base.input.InputEventDetectionSource
@@ -11,7 +12,11 @@ import io.github.sds100.keymapper.base.trigger.AssistantTriggerType
import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey
import io.github.sds100.keymapper.base.trigger.RecordTriggerController
import io.github.sds100.keymapper.base.trigger.RecordTriggerState
-import io.github.sds100.keymapper.base.trigger.Trigger
+import io.github.sds100.keymapper.common.models.EvdevDeviceInfo
+import io.github.sds100.keymapper.common.models.GrabTargetKeyCode
+import io.github.sds100.keymapper.data.Keys
+import io.github.sds100.keymapper.data.PreferenceDefaults
+import io.github.sds100.keymapper.data.repositories.PreferenceRepository
import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent
import io.github.sds100.keymapper.system.inputevents.KMInputEvent
import kotlinx.coroutines.CoroutineScope
@@ -19,6 +24,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -31,11 +37,61 @@ class KeyMapDetectionController(
private val inputEventHub: InputEventHub,
private val pauseKeyMapsUseCase: PauseKeyMapsUseCase,
private val recordTriggerController: RecordTriggerController,
+ private val preferences: PreferenceRepository,
) : InputEventHubCallback {
companion object {
private const val INPUT_EVENT_HUB_ID = "key_map_controller"
+
+ fun getEvdevGrabRequests(
+ algorithm: KeyMapAlgorithm,
+ injectKeyEventActionsWithSystemBridge: Boolean = true,
+ ): List {
+ val deviceKeyEventMap = mutableMapOf>()
+
+ for ((index, trigger) in algorithm.triggers.withIndex()) {
+ val evdevDevices = trigger.keys.filterIsInstance()
+ .map { it.device }
+ .distinct()
+ .toList()
+
+ if (evdevDevices.isEmpty()) {
+ continue
+ }
+
+ val actions: List = algorithm.triggerActions[index]
+ .map { actionIndex -> algorithm.actionMap[actionIndex]?.data }
+ .filterNotNull()
+
+ val extraKeyCodes = if (injectKeyEventActionsWithSystemBridge) {
+ actions
+ .filterIsInstance()
+ .map { it.keyCode }
+ } else {
+ emptyList()
+ }
+
+ for (device in evdevDevices) {
+ deviceKeyEventMap.getOrPut(device) { mutableSetOf() }.addAll(extraKeyCodes)
+ }
+ }
+
+ return deviceKeyEventMap.map { (device, keyEvents) ->
+ GrabTargetKeyCode(
+ name = device.name,
+ bus = device.bus,
+ vendor = device.vendor,
+ product = device.product,
+ extraKeyCodes = keyEvents.toIntArray(),
+ )
+ }
+ }
}
+ private val injectKeyEventsWithSystemBridge: StateFlow =
+ preferences.get(Keys.keyEventActionsUseSystemBridge)
+ .map { it ?: PreferenceDefaults.KEY_EVENT_ACTIONS_USE_SYSTEM_BRIDGE }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, false)
+
private val algorithm: KeyMapAlgorithm =
KeyMapAlgorithm(coroutineScope, detectUseCase, performActionsUseCase, detectConstraints)
@@ -43,25 +99,41 @@ class KeyMapDetectionController(
pauseKeyMapsUseCase.isPaused.stateIn(coroutineScope, SharingStarted.Eagerly, false)
init {
- // Must first register before collecting anything that may call reset()
+ // Must first register before collecting anything that may change the grabbed
+ // evdev devices.
inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this, listOf(KMEvdevEvent.TYPE_KEY_EVENT))
coroutineScope.launch {
combine(detectUseCase.allKeyMapList, isPaused) { keyMapList, isPaused ->
- algorithm.reset()
-
- if (isPaused) {
- algorithm.loadKeyMaps(emptyList())
- inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList())
- } else {
- algorithm.loadKeyMaps(keyMapList)
- // Only grab the triggers that are actually being listened to by the algorithm
- grabEvdevDevicesForTriggers(algorithm.triggers)
- }
+ invalidateState(keyMapList, isPaused)
}.launchIn(coroutineScope)
}
}
+ private fun invalidateState(keyMapList: List, isPaused: Boolean) {
+ algorithm.reset()
+
+ if (isPaused) {
+ algorithm.loadKeyMaps(emptyList())
+ inputEventHub.setGrabTargets(INPUT_EVENT_HUB_ID, emptyList())
+ } else {
+ algorithm.loadKeyMaps(keyMapList)
+ // Determine which evdev devices need to be grabbed depending on the state
+ // of the algorithm.
+ val grabRequests =
+ getEvdevGrabRequests(algorithm, injectKeyEventsWithSystemBridge.value)
+
+ Timber.i(
+ "Grab evdev devices for key map detection: ${grabRequests.joinToString()}",
+ )
+
+ inputEventHub.setGrabTargets(
+ INPUT_EVENT_HUB_ID,
+ grabRequests,
+ )
+ }
+ }
+
override fun onInputEvent(
event: KMInputEvent,
detectionSource: InputEventDetectionSource,
@@ -94,26 +166,8 @@ class KeyMapDetectionController(
}
fun teardown() {
- reset()
- inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID)
- }
-
- private fun grabEvdevDevicesForTriggers(triggers: Array) {
- val evdevDevices = triggers
- .flatMap { trigger -> trigger.keys.filterIsInstance() }
- .map { it.device }
- .distinct()
- .toList()
-
- Timber.i("Grab evdev devices for key map detection: ${evdevDevices.joinToString()}")
- inputEventHub.setGrabbedEvdevDevices(
- INPUT_EVENT_HUB_ID,
- evdevDevices,
- )
- }
-
- private fun reset() {
algorithm.reset()
- inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList())
+ inputEventHub.setGrabTargets(INPUT_EVENT_HUB_ID, emptyList())
+ inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID)
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt
index a4b46b0833..ad9e255233 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt
@@ -2,6 +2,7 @@ package io.github.sds100.keymapper.base.detection
import io.github.sds100.keymapper.base.actions.Action
import io.github.sds100.keymapper.base.actions.ActionData
+import io.github.sds100.keymapper.base.actions.PerformActionTriggerDevice
import io.github.sds100.keymapper.base.actions.PerformActionsUseCase
import io.github.sds100.keymapper.base.actions.RepeatMode
import io.github.sds100.keymapper.common.utils.InputEventAction
@@ -47,7 +48,11 @@ class ParallelTriggerActionPerformer(
PreferenceDefaults.REPEAT_RATE.toLong(),
)
- fun onTriggered(calledOnTriggerRelease: Boolean, metaState: Int) {
+ fun onTriggered(
+ calledOnTriggerRelease: Boolean,
+ metaState: Int,
+ device: PerformActionTriggerDevice,
+ ) {
performActionsJob?.cancel()
/*
this job shouldn't be cancelled when the trigger is released. all actions should be performed
@@ -84,7 +89,7 @@ class ParallelTriggerActionPerformer(
else -> InputEventAction.DOWN_UP
}
- performAction(action, actionInputEventAction, metaState)
+ performAction(action, actionInputEventAction, metaState, device)
if (action.repeat && action.holdDown) {
delay(action.holdDownDuration?.toLong() ?: defaultHoldDownDuration.value)
@@ -131,13 +136,13 @@ class ParallelTriggerActionPerformer(
while (isActive && continueRepeating) {
if (action.holdDown) {
- performAction(action, InputEventAction.DOWN, metaState)
+ performAction(action, InputEventAction.DOWN, metaState, device)
delay(
action.holdDownDuration?.toLong() ?: defaultHoldDownDuration.value,
)
- performAction(action, InputEventAction.UP, metaState)
+ performAction(action, InputEventAction.UP, metaState, device)
} else {
- performAction(action, InputEventAction.DOWN_UP, metaState)
+ performAction(action, InputEventAction.DOWN_UP, metaState, device)
}
repeatCount++
@@ -152,7 +157,7 @@ class ParallelTriggerActionPerformer(
}
}
- fun onReleased(metaState: Int) {
+ fun onReleased(metaState: Int, device: PerformActionTriggerDevice) {
repeatJobs.forEachIndexed { actionIndex, job ->
if (actionList[actionIndex].repeatMode == RepeatMode.TRIGGER_RELEASED) {
job?.cancel()
@@ -166,7 +171,7 @@ class ParallelTriggerActionPerformer(
if (actionIsHeldDown[actionIndex]) {
actionIsHeldDown[actionIndex] = false
- performAction(action, InputEventAction.UP, metaState)
+ performAction(action, InputEventAction.UP, metaState, device)
}
}
}
@@ -180,7 +185,12 @@ class ParallelTriggerActionPerformer(
coroutineScope.launch {
for ((index, isHeldDown) in actionIsHeldDown.withIndex()) {
if (isHeldDown) {
- performAction(actionList[index], inputEventAction = InputEventAction.UP, 0)
+ performAction(
+ actionList[index],
+ inputEventAction = InputEventAction.UP,
+ 0,
+ PerformActionTriggerDevice.Default,
+ )
}
}
}
@@ -199,9 +209,10 @@ class ParallelTriggerActionPerformer(
action: Action,
inputEventAction: InputEventAction,
metaState: Int,
+ device: PerformActionTriggerDevice,
) {
repeat(action.multiplier ?: 1) {
- useCase.perform(action.data, inputEventAction, metaState)
+ useCase.perform(action.data, inputEventAction, metaState, device)
}
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt
index dc7d40816b..c17dc2ffdf 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt
@@ -1,6 +1,7 @@
package io.github.sds100.keymapper.base.detection
import io.github.sds100.keymapper.base.actions.Action
+import io.github.sds100.keymapper.base.actions.PerformActionTriggerDevice
import io.github.sds100.keymapper.base.actions.PerformActionsUseCase
import io.github.sds100.keymapper.common.utils.InputEventAction
import kotlinx.coroutines.CoroutineScope
@@ -15,7 +16,7 @@ class SequenceTriggerActionPerformer(
) {
private var job: Job? = null
- fun onTriggered(metaState: Int) {
+ fun onTriggered(device: PerformActionTriggerDevice, metaState: Int) {
/*
this job shouldn't be cancelled when the trigger is released. all actions should be performed
once before repeating (if configured).
@@ -23,7 +24,7 @@ class SequenceTriggerActionPerformer(
job?.cancel()
job = coroutineScope.launch {
for (action in actionList) {
- performAction(action, metaState)
+ performAction(action, metaState, device)
delay(action.delayBeforeNextAction?.toLong() ?: 0L)
}
@@ -35,9 +36,13 @@ class SequenceTriggerActionPerformer(
job = null
}
- private suspend fun performAction(action: Action, metaState: Int) {
+ private suspend fun performAction(
+ action: Action,
+ metaState: Int,
+ device: PerformActionTriggerDevice,
+ ) {
repeat(action.multiplier ?: 1) {
- useCase.perform(action.data, InputEventAction.DOWN_UP, metaState)
+ useCase.perform(action.data, InputEventAction.DOWN_UP, metaState, device)
}
}
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt
similarity index 81%
rename from base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt
rename to base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt
index 719c2cfabb..496c22c917 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeScreen.kt
@@ -1,4 +1,4 @@
-package io.github.sds100.keymapper.base.promode
+package io.github.sds100.keymapper.base.expertmode
import android.os.Build
import android.provider.Settings
@@ -44,6 +44,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
@@ -75,20 +76,20 @@ import io.github.sds100.keymapper.common.utils.SettingsUtils
import io.github.sds100.keymapper.common.utils.State
@Composable
-fun ProModeScreen(modifier: Modifier = Modifier, viewModel: ProModeViewModel) {
- val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle()
- val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle()
+fun ExpertModeScreen(modifier: Modifier = Modifier, viewModel: ExpertModeViewModel) {
+ val expertModeWarningState by viewModel.warningState.collectAsStateWithLifecycle()
+ val expertModeSetupState by viewModel.setupState.collectAsStateWithLifecycle()
val autoStartBootEnabled by viewModel.autoStartBootEnabled.collectAsStateWithLifecycle()
- ProModeScreen(
+ ExpertModeScreen(
modifier = modifier,
onBackClick = viewModel::onBackClick,
onHelpClick = { viewModel.showInfoCard() },
showHelpIcon = !viewModel.showInfoCard,
) {
Content(
- warningState = proModeWarningState,
- setupState = proModeSetupState,
+ warningState = expertModeWarningState,
+ setupState = expertModeSetupState,
showInfoCard = viewModel.showInfoCard,
onInfoCardDismiss = { viewModel.hideInfoCard() },
onWarningButtonClick = viewModel::onWarningButtonClick,
@@ -105,7 +106,7 @@ fun ProModeScreen(modifier: Modifier = Modifier, viewModel: ProModeViewModel) {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-private fun ProModeScreen(
+private fun ExpertModeScreen(
modifier: Modifier = Modifier,
onBackClick: () -> Unit = {},
onHelpClick: () -> Unit = {},
@@ -116,7 +117,7 @@ private fun ProModeScreen(
modifier = modifier.displayCutoutPadding(),
topBar = {
TopAppBar(
- title = { Text(stringResource(R.string.pro_mode_app_bar_title)) },
+ title = { Text(stringResource(R.string.expert_mode_app_bar_title)) },
actions = {
AnimatedVisibility(
visible = showHelpIcon,
@@ -127,7 +128,7 @@ private fun ProModeScreen(
Icon(
imageVector = Icons.AutoMirrored.Rounded.HelpOutline,
contentDescription = stringResource(
- R.string.pro_mode_info_card_show_content_description,
+ R.string.expert_mode_info_card_show_content_description,
),
)
}
@@ -168,8 +169,8 @@ private fun ProModeScreen(
@Composable
private fun Content(
modifier: Modifier = Modifier,
- warningState: ProModeWarningState,
- setupState: State,
+ warningState: ExpertModeWarningState,
+ setupState: State,
showInfoCard: Boolean,
onInfoCardDismiss: () -> Unit = {},
onWarningButtonClick: () -> Unit = {},
@@ -187,7 +188,7 @@ private fun Content(
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
- ProModeInfoCard(
+ ExpertModeInfoCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
@@ -209,7 +210,7 @@ private fun Content(
Spacer(modifier = Modifier.height(16.dp))
- if (warningState is ProModeWarningState.Understood) {
+ if (warningState is ExpertModeWarningState.Understood) {
when (setupState) {
is State.Loading -> {
CircularProgressIndicator(
@@ -234,7 +235,7 @@ private fun Content(
} else {
Text(
modifier = Modifier.padding(horizontal = 32.dp),
- text = stringResource(R.string.pro_mode_settings_unavailable_text),
+ text = stringResource(R.string.expert_mode_settings_unavailable_text),
textAlign = TextAlign.Center,
)
}
@@ -244,7 +245,7 @@ private fun Content(
@Composable
private fun LoadedContent(
modifier: Modifier,
- state: ProModeState,
+ state: ExpertModeState,
onRootButtonClick: () -> Unit = {},
onShizukuButtonClick: () -> Unit,
onStopServiceClick: () -> Unit,
@@ -257,16 +258,16 @@ private fun LoadedContent(
OptionsHeaderRow(
modifier = Modifier.padding(horizontal = 16.dp),
icon = Icons.Rounded.Checklist,
- text = stringResource(R.string.pro_mode_set_up_title),
+ text = stringResource(R.string.expert_mode_set_up_title),
)
Spacer(modifier = Modifier.height(8.dp))
// Show notification permission warning if permission not granted
- if (state is ProModeState.Stopped && !state.isNotificationPermissionGranted) {
+ if (state is ExpertModeState.Stopped && !state.isNotificationPermissionGranted) {
val text =
stringResource(
- R.string.pro_mode_setup_wizard_enable_notification_permission_description,
+ R.string.expert_mode_setup_wizard_enable_notification_permission_description,
)
SetupCard(
@@ -282,7 +283,7 @@ private fun LoadedContent(
)
},
title = stringResource(
- R.string.pro_mode_setup_wizard_enable_notification_permission_title,
+ R.string.expert_mode_setup_wizard_enable_notification_permission_title,
),
content = {
Text(
@@ -291,7 +292,7 @@ private fun LoadedContent(
)
},
buttonText = stringResource(
- R.string.pro_mode_setup_wizard_enable_notification_permission_button,
+ R.string.expert_mode_setup_wizard_enable_notification_permission_button,
),
onButtonClick = onRequestNotificationPermissionClick,
)
@@ -299,7 +300,7 @@ private fun LoadedContent(
}
when (state) {
- is ProModeState.Started -> {
+ is ExpertModeState.Started -> {
if (!state.isDefaultUsbModeCompatible) {
IncompatibleUsbModeCard(
modifier = Modifier
@@ -310,7 +311,7 @@ private fun LoadedContent(
Spacer(Modifier.height(8.dp))
}
- ProModeStartedCard(
+ ExpertModeStartedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
@@ -318,7 +319,7 @@ private fun LoadedContent(
)
}
- is ProModeState.Stopped -> {
+ is ExpertModeState.Stopped -> {
if (state.isRootGranted) {
SetupCard(
modifier = Modifier
@@ -332,18 +333,19 @@ private fun LoadedContent(
tint = LocalCustomColorsPalette.current.magiskTeal,
)
},
- title = stringResource(R.string.pro_mode_root_detected_title),
+ title = stringResource(R.string.expert_mode_root_detected_title),
content = {
Text(
- text = stringResource(R.string.pro_mode_root_detected_text),
+ text = stringResource(R.string.expert_mode_root_detected_text),
style = MaterialTheme.typography.bodyMedium,
)
},
buttonText = stringResource(
- R.string.pro_mode_root_detected_button_start_service,
+ R.string.expert_mode_root_detected_button_start_service,
),
onButtonClick = onRootButtonClick,
enabled = state.isNotificationPermissionGranted,
+ isLoading = state.isStarting,
)
Spacer(modifier = Modifier.height(8.dp))
@@ -351,14 +353,17 @@ private fun LoadedContent(
val shizukuButtonText: String? = when (state.shizukuSetupState) {
ShizukuSetupState.INSTALLED -> stringResource(
- R.string.pro_mode_shizuku_detected_button_start,
+ R.string.expert_mode_shizuku_detected_button_start,
)
+
ShizukuSetupState.STARTED -> stringResource(
- R.string.pro_mode_shizuku_detected_button_request_permission,
+ R.string.expert_mode_shizuku_detected_button_request_permission,
)
+
ShizukuSetupState.PERMISSION_GRANTED -> stringResource(
- R.string.pro_mode_shizuku_detected_button_start_service,
+ R.string.expert_mode_shizuku_detected_button_start_service,
)
+
ShizukuSetupState.NOT_FOUND -> null
}
@@ -374,16 +379,17 @@ private fun LoadedContent(
contentDescription = null,
)
},
- title = stringResource(R.string.pro_mode_shizuku_detected_title),
+ title = stringResource(R.string.expert_mode_shizuku_detected_title),
content = {
Text(
- text = stringResource(R.string.pro_mode_shizuku_detected_text),
+ text = stringResource(R.string.expert_mode_shizuku_detected_text),
style = MaterialTheme.typography.bodyMedium,
)
},
buttonText = shizukuButtonText,
onButtonClick = onShizukuButtonClick,
enabled = state.isNotificationPermissionGranted,
+ isLoading = state.isStarting,
)
}
@@ -391,9 +397,10 @@ private fun LoadedContent(
val setupKeyMapperText: String = when {
Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> stringResource(
- R.string.pro_mode_set_up_with_key_mapper_button_incompatible,
+ R.string.expert_mode_set_up_with_key_mapper_button_incompatible,
)
- else -> stringResource(R.string.pro_mode_set_up_with_key_mapper_button)
+
+ else -> stringResource(R.string.expert_mode_set_up_with_key_mapper_button)
}
SetupCard(
@@ -408,13 +415,14 @@ private fun LoadedContent(
contentDescription = null,
)
},
- title = stringResource(R.string.pro_mode_set_up_with_key_mapper_title),
+ title = stringResource(R.string.expert_mode_set_up_with_key_mapper_title),
content = {},
buttonText = setupKeyMapperText,
onButtonClick = onSetupWithKeyMapperClick,
enabled =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
state.isNotificationPermissionGranted,
+ isLoading = state.isStarting,
)
}
}
@@ -425,15 +433,15 @@ private fun LoadedContent(
OptionsHeaderRow(
modifier = Modifier.padding(horizontal = 16.dp),
icon = Icons.Rounded.Tune,
- text = stringResource(R.string.pro_mode_options_title),
+ text = stringResource(R.string.expert_mode_options_title),
)
Spacer(modifier = Modifier.height(8.dp))
SwitchPreferenceCompose(
modifier = Modifier.padding(horizontal = 8.dp),
- title = stringResource(R.string.title_pref_pro_mode_auto_start_at_boot),
- text = stringResource(R.string.summary_pref_pro_mode_auto_start_at_boot),
+ title = stringResource(R.string.title_pref_expert_mode_auto_start),
+ text = stringResource(R.string.summary_pref_expert_mode_auto_start),
icon = Icons.Rounded.RestartAlt,
isChecked = autoStartAtBoot,
onCheckedChange = onAutoStartAtBootToggled,
@@ -457,12 +465,12 @@ private fun IncompatibleUsbModeCard(modifier: Modifier = Modifier) {
)
},
title = stringResource(
- R.string.pro_mode_setup_wizard_change_default_usb_configuration_title,
+ R.string.expert_mode_setup_wizard_change_default_usb_configuration_title,
),
content = {
Text(
text = stringResource(
- R.string.pro_mode_setup_wizard_change_default_usb_configuration_description,
+ R.string.expert_mode_setup_wizard_change_default_usb_configuration_description,
),
style = MaterialTheme.typography.bodyMedium,
)
@@ -484,10 +492,10 @@ private fun IncompatibleUsbModeCard(modifier: Modifier = Modifier) {
@Composable
private fun WarningCard(
modifier: Modifier = Modifier,
- state: ProModeWarningState,
+ state: ExpertModeWarningState,
onButtonClick: () -> Unit = {},
) {
- val borderStroke = if (state is ProModeWarningState.Understood) {
+ val borderStroke = if (state is ExpertModeWarningState.Understood) {
CardDefaults.outlinedCardBorder()
} else {
BorderStroke(1.dp, MaterialTheme.colorScheme.error)
@@ -509,7 +517,7 @@ private fun WarningCard(
Spacer(modifier = Modifier.width(8.dp))
Text(
- text = stringResource(R.string.pro_mode_warning_title),
+ text = stringResource(R.string.expert_mode_warning_title),
style = MaterialTheme.typography.titleMedium,
)
}
@@ -518,7 +526,7 @@ private fun WarningCard(
Text(
modifier = Modifier.padding(horizontal = 16.dp),
- text = stringResource(R.string.pro_mode_warning_text),
+ text = stringResource(R.string.expert_mode_warning_text),
style = MaterialTheme.typography.bodyMedium,
)
@@ -529,29 +537,30 @@ private fun WarningCard(
.align(Alignment.End)
.padding(horizontal = 16.dp),
onClick = onButtonClick,
- enabled = state is ProModeWarningState.Idle,
+ enabled = state is ExpertModeWarningState.Idle,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError,
),
) {
- if (state is ProModeWarningState.Understood) {
+ if (state is ExpertModeWarningState.Understood) {
Icon(imageVector = Icons.Rounded.Check, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
}
val text = when (state) {
- is ProModeWarningState.CountingDown -> stringResource(
- R.string.pro_mode_warning_understand_button_countdown,
+ is ExpertModeWarningState.CountingDown -> stringResource(
+ R.string.expert_mode_warning_understand_button_countdown,
state.seconds,
)
- ProModeWarningState.Idle -> stringResource(
- R.string.pro_mode_warning_understand_button_not_completed,
+ ExpertModeWarningState.Idle -> stringResource(
+ R.string.expert_mode_warning_understand_button_not_completed,
)
- ProModeWarningState.Understood -> stringResource(
- R.string.pro_mode_warning_understand_button_completed,
+
+ ExpertModeWarningState.Understood -> stringResource(
+ R.string.expert_mode_warning_understand_button_completed,
)
}
@@ -563,7 +572,7 @@ private fun WarningCard(
}
@Composable
-private fun ProModeStartedCard(modifier: Modifier = Modifier, onStopClick: () -> Unit = {}) {
+private fun ExpertModeStartedCard(modifier: Modifier = Modifier, onStopClick: () -> Unit = {}) {
OutlinedCard(modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -582,7 +591,7 @@ private fun ProModeStartedCard(modifier: Modifier = Modifier, onStopClick: () ->
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp),
- text = stringResource(R.string.pro_mode_service_started),
+ text = stringResource(R.string.expert_mode_service_started),
style = MaterialTheme.typography.titleMedium,
)
@@ -594,7 +603,7 @@ private fun ProModeStartedCard(modifier: Modifier = Modifier, onStopClick: () ->
contentColor = MaterialTheme.colorScheme.error,
),
) {
- Text(stringResource(R.string.pro_mode_stop_service_button))
+ Text(stringResource(R.string.expert_mode_stop_service_button))
}
Spacer(modifier = Modifier.width(16.dp))
@@ -612,6 +621,7 @@ private fun SetupCard(
buttonText: String,
onButtonClick: () -> Unit = {},
enabled: Boolean = true,
+ isLoading: Boolean = false,
) {
OutlinedCard(modifier = modifier) {
Spacer(modifier = Modifier.height(16.dp))
@@ -645,12 +655,20 @@ private fun SetupCard(
.align(Alignment.End)
.padding(horizontal = 16.dp),
onClick = onButtonClick,
- enabled = enabled,
+ enabled = enabled && !isLoading,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = color,
contentColor = LocalCustomColorsPalette.current.contentColorFor(color),
),
) {
+ if (isLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(18.dp),
+ strokeWidth = 2.dp,
+ color = LocalContentColor.current,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ }
Text(buttonText)
}
@@ -659,7 +677,7 @@ private fun SetupCard(
}
@Composable
-private fun ProModeInfoCard(modifier: Modifier = Modifier, onDismiss: () -> Unit = {}) {
+private fun ExpertModeInfoCard(modifier: Modifier = Modifier, onDismiss: () -> Unit = {}) {
OutlinedCard(
modifier = modifier,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary),
@@ -682,7 +700,7 @@ private fun ProModeInfoCard(modifier: Modifier = Modifier, onDismiss: () -> Unit
Spacer(modifier = Modifier.width(8.dp))
Text(
- text = stringResource(R.string.pro_mode_info_card_title),
+ text = stringResource(R.string.expert_mode_info_card_title),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
@@ -691,7 +709,7 @@ private fun ProModeInfoCard(modifier: Modifier = Modifier, onDismiss: () -> Unit
Spacer(modifier = Modifier.height(8.dp))
Text(
- text = stringResource(R.string.pro_mode_info_card_description),
+ text = stringResource(R.string.expert_mode_info_card_description),
style = MaterialTheme.typography.bodyMedium,
)
}
@@ -705,7 +723,7 @@ private fun ProModeInfoCard(modifier: Modifier = Modifier, onDismiss: () -> Unit
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(
- R.string.pro_mode_info_card_dismiss_content_description,
+ R.string.expert_mode_info_card_dismiss_content_description,
),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -718,14 +736,15 @@ private fun ProModeInfoCard(modifier: Modifier = Modifier, onDismiss: () -> Unit
@Composable
private fun Preview() {
KeyMapperTheme {
- ProModeScreen {
+ ExpertModeScreen {
Content(
- warningState = ProModeWarningState.Understood,
+ warningState = ExpertModeWarningState.Understood,
setupState = State.Data(
- ProModeState.Stopped(
+ ExpertModeState.Stopped(
isRootGranted = false,
shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED,
isNotificationPermissionGranted = true,
+ isStarting = false,
),
),
showInfoCard = true,
@@ -741,10 +760,10 @@ private fun Preview() {
@Composable
private fun PreviewDark() {
KeyMapperTheme(darkTheme = true) {
- ProModeScreen {
+ ExpertModeScreen {
Content(
- warningState = ProModeWarningState.Understood,
- setupState = State.Data(ProModeState.Started(isDefaultUsbModeCompatible = true)),
+ warningState = ExpertModeWarningState.Understood,
+ setupState = State.Data(ExpertModeState.Started(isDefaultUsbModeCompatible = true)),
showInfoCard = false,
onInfoCardDismiss = {},
autoStartAtBoot = true,
@@ -758,9 +777,9 @@ private fun PreviewDark() {
@Composable
private fun PreviewCountingDown() {
KeyMapperTheme {
- ProModeScreen {
+ ExpertModeScreen {
Content(
- warningState = ProModeWarningState.CountingDown(
+ warningState = ExpertModeWarningState.CountingDown(
seconds = 5,
),
setupState = State.Loading,
@@ -777,10 +796,12 @@ private fun PreviewCountingDown() {
@Composable
private fun PreviewStarted() {
KeyMapperTheme {
- ProModeScreen {
+ ExpertModeScreen {
Content(
- warningState = ProModeWarningState.Understood,
- setupState = State.Data(ProModeState.Started(isDefaultUsbModeCompatible = false)),
+ warningState = ExpertModeWarningState.Understood,
+ setupState = State.Data(
+ ExpertModeState.Started(isDefaultUsbModeCompatible = false),
+ ),
showInfoCard = false,
onInfoCardDismiss = {},
autoStartAtBoot = false,
@@ -794,14 +815,15 @@ private fun PreviewStarted() {
@Composable
private fun PreviewNotificationPermissionNotGranted() {
KeyMapperTheme {
- ProModeScreen {
+ ExpertModeScreen {
Content(
- warningState = ProModeWarningState.Understood,
+ warningState = ExpertModeWarningState.Understood,
setupState = State.Data(
- ProModeState.Stopped(
+ ExpertModeState.Stopped(
isRootGranted = true,
shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED,
isNotificationPermissionGranted = false,
+ isStarting = false,
),
),
showInfoCard = false,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupDelegateImpl.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupDelegateImpl.kt
new file mode 100644
index 0000000000..2d1c491ad9
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupDelegateImpl.kt
@@ -0,0 +1,28 @@
+package io.github.sds100.keymapper.base.expertmode
+
+import dagger.hilt.android.scopes.ViewModelScoped
+import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
+import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@ViewModelScoped
+class ExpertModeSetupDelegateImpl @Inject constructor(
+ @Named("viewmodel")
+ viewModelScope: CoroutineScope,
+ useCase: SystemBridgeSetupUseCase,
+ resourceProvider: ResourceProvider,
+ private val navigationProvider: NavigationProvider,
+) : SystemBridgeSetupDelegateImpl(
+ viewModelScope,
+ useCase,
+ resourceProvider,
+) {
+ override fun onFinishClick() {
+ viewModelScope.launch {
+ navigationProvider.popBackStack()
+ }
+ }
+}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupScreen.kt
similarity index 50%
rename from base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt
rename to base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupScreen.kt
index 8b49b0cd9a..aba3e9398d 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupScreen.kt
@@ -1,4 +1,4 @@
-package io.github.sds100.keymapper.base.promode
+package io.github.sds100.keymapper.base.expertmode
import android.content.res.Configuration
import androidx.compose.animation.core.Animatable
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -61,13 +62,13 @@ import io.github.sds100.keymapper.common.utils.State
import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep
@Composable
-fun ProModeSetupScreen(viewModel: ProModeSetupViewModel) {
+fun ExpertModeSetupScreen(viewModel: ExpertModeSetupViewModel) {
val state by viewModel.setupState.collectAsStateWithLifecycle()
- ProModeSetupScreen(
+ ExpertModeSetupScreen(
state = state,
- onStepButtonClick = viewModel::onStepButtonClick,
- onAssistantClick = viewModel::onAssistantClick,
+ onStepButtonClick = viewModel::onSetupStepButtonClick,
+ onAssistantClick = viewModel::onSetupAssistantClick,
onWatchTutorialClick = { },
onBackClick = viewModel::onBackClick,
)
@@ -75,8 +76,8 @@ fun ProModeSetupScreen(viewModel: ProModeSetupViewModel) {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun ProModeSetupScreen(
- state: State,
+fun ExpertModeSetupScreen(
+ state: State,
onBackClick: () -> Unit = {},
onStepButtonClick: () -> Unit = {},
onAssistantClick: () -> Unit = {},
@@ -85,7 +86,7 @@ fun ProModeSetupScreen(
Scaffold(
topBar = {
TopAppBar(
- title = { Text(stringResource(R.string.pro_mode_setup_wizard_title)) },
+ title = { Text(stringResource(R.string.expert_mode_setup_wizard_title)) },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
@@ -97,105 +98,121 @@ fun ProModeSetupScreen(
)
},
) { paddingValues ->
- when (state) {
- State.Loading -> {
- Box(
- Modifier
- .padding(paddingValues)
- .fillMaxSize(),
- contentAlignment = Alignment.Center,
- ) {
- CircularProgressIndicator()
- }
+ ExpertModeSetupScreenContent(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize(),
+ state,
+ onAssistantClick,
+ onWatchTutorialClick,
+ onStepButtonClick,
+ )
+ }
+}
+
+@Composable
+fun ExpertModeSetupScreenContent(
+ modifier: Modifier = Modifier,
+ state: State,
+ onAssistantClick: () -> Unit,
+ onWatchTutorialClick: () -> Unit,
+ onStepButtonClick: () -> Unit,
+) {
+ when (state) {
+ State.Loading -> {
+ Box(
+ modifier,
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator()
}
+ }
- is State.Data -> {
- val stepContent = getStepContent(state.data.step)
+ is State.Data -> {
+ val stepContent = state.data.stepContent
+
+ // Create animated progress for entrance and updates
+ val progressAnimatable = remember { Animatable(0f) }
+ val targetProgress = state.data.stepNumber.toFloat() / (state.data.stepCount)
+
+ // Animate progress when it changes
+ LaunchedEffect(targetProgress) {
+ progressAnimatable.animateTo(
+ targetValue = targetProgress,
+ animationSpec = tween(
+ durationMillis = 800,
+ easing = EaseInOut,
+ ),
+ )
+ }
- // Create animated progress for entrance and updates
- val progressAnimatable = remember { Animatable(0f) }
- val targetProgress = state.data.stepNumber.toFloat() / (state.data.stepCount)
+ // Animate entrance when screen opens
+ LaunchedEffect(Unit) {
+ progressAnimatable.animateTo(
+ targetValue = targetProgress,
+ animationSpec = tween(
+ durationMillis = 1000,
+ easing = EaseInOut,
+ ),
+ )
+ }
- // Animate progress when it changes
- LaunchedEffect(targetProgress) {
- progressAnimatable.animateTo(
- targetValue = targetProgress,
- animationSpec = tween(
- durationMillis = 800,
- easing = EaseInOut,
- ),
- )
- }
+ Column(
+ modifier = modifier
+ .padding(vertical = 16.dp, horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ LinearProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ progress = { progressAnimatable.value },
+ )
- // Animate entrance when screen opens
- LaunchedEffect(Unit) {
- progressAnimatable.animateTo(
- targetValue = targetProgress,
- animationSpec = tween(
- durationMillis = 1000,
- easing = EaseInOut,
- ),
- )
- }
+ Spacer(modifier = Modifier.height(16.dp))
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
- .padding(vertical = 16.dp, horizontal = 16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
) {
- LinearProgressIndicator(
- modifier = Modifier.fillMaxWidth(),
- progress = { progressAnimatable.value },
+ Text(
+ text = stringResource(
+ R.string.expert_mode_setup_wizard_step_n,
+ state.data.stepNumber,
+ state.data.stepCount,
+ ),
+ style = MaterialTheme.typography.titleLarge,
)
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = stringResource(
- R.string.pro_mode_setup_wizard_step_n,
- state.data.stepNumber,
- state.data.stepCount,
- ),
- style = MaterialTheme.typography.titleLarge,
- )
- Text(
- text = stringResource(R.string.pro_mode_app_bar_title),
- style = MaterialTheme.typography.titleLarge,
- )
- }
- Spacer(modifier = Modifier.height(16.dp))
-
- AssistantCheckBoxRow(
- modifier = Modifier.fillMaxWidth(),
- isEnabled = state.data.isSetupAssistantButtonEnabled,
- isChecked = state.data.isSetupAssistantChecked,
- onAssistantClick = onAssistantClick,
+ Text(
+ text = stringResource(R.string.expert_mode_setup_title),
+ style = MaterialTheme.typography.titleLarge,
)
+ }
+ Spacer(modifier = Modifier.height(16.dp))
- val iconTint = if (state.data.step == SystemBridgeSetupStep.STARTED) {
- LocalCustomColorsPalette.current.green
- } else {
- MaterialTheme.colorScheme.onSurface
- }
+ AssistantCheckBoxRow(
+ modifier = Modifier.fillMaxWidth(),
+ isEnabled = state.data.isSetupAssistantButtonEnabled,
+ isChecked = state.data.isSetupAssistantChecked,
+ onAssistantClick = onAssistantClick,
+ )
- StepContent(
- modifier = Modifier
- .fillMaxWidth()
- .weight(1f)
- .padding(horizontal = 16.dp),
- stepContent,
- onWatchTutorialClick,
- onStepButtonClick,
- iconTint = iconTint,
- )
+ val iconTint = if (state.data.step == SystemBridgeSetupStep.STARTED) {
+ LocalCustomColorsPalette.current.green
+ } else {
+ MaterialTheme.colorScheme.onSurface
}
+
+ StepContent(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(horizontal = 16.dp),
+ stepContent = stepContent,
+ onWatchTutorialClick = onWatchTutorialClick,
+ onButtonClick = onStepButtonClick,
+ iconTint = iconTint,
+ isLoading = state.data.isStarting,
+ )
}
}
}
@@ -208,6 +225,7 @@ private fun StepContent(
onWatchTutorialClick: () -> Unit,
onButtonClick: () -> Unit,
iconTint: Color = Color.Unspecified,
+ isLoading: Boolean = false,
) {
Column(
modifier,
@@ -252,9 +270,20 @@ private fun StepContent(
verticalAlignment = Alignment.CenterVertically,
) {
// TextButton(onClick = onWatchTutorialClick) {
-// Text(text = stringResource(R.string.pro_mode_setup_wizard_watch_tutorial_button))
+// Text(text = stringResource(R.string.expert_mode_setup_wizard_watch_tutorial_button))
// }
- Button(onClick = onButtonClick) {
+ Button(
+ onClick = onButtonClick,
+ enabled = !isLoading,
+ ) {
+ if (isLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(18.dp),
+ strokeWidth = 2.dp,
+ color = LocalContentColor.current,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ }
Text(text = stepContent.buttonText)
}
}
@@ -292,16 +321,16 @@ private fun AssistantCheckBoxRow(
)
val text = if (isEnabled) {
- stringResource(R.string.pro_mode_setup_wizard_use_assistant_description)
+ stringResource(R.string.expert_mode_setup_wizard_use_assistant_description)
} else {
stringResource(
- R.string.pro_mode_setup_wizard_use_assistant_enable_accessibility_service,
+ R.string.expert_mode_setup_wizard_use_assistant_enable_service,
)
}
Column {
Text(
- text = stringResource(R.string.pro_mode_setup_wizard_use_assistant),
+ text = stringResource(R.string.expert_mode_setup_wizard_use_assistant),
style = MaterialTheme.typography.titleMedium,
)
@@ -316,103 +345,118 @@ private fun AssistantCheckBoxRow(
}
@Composable
-private fun getStepContent(step: SystemBridgeSetupStep): StepContent {
+private fun getIconForStep(step: SystemBridgeSetupStep): ImageVector {
+ return when (step) {
+ SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> Icons.Rounded.Accessibility
+ SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> Icons.Rounded.Notifications
+ SystemBridgeSetupStep.DEVELOPER_OPTIONS -> Icons.Rounded.Build
+ SystemBridgeSetupStep.WIFI_NETWORK -> KeyMapperIcons.SignalWifiNotConnected
+ SystemBridgeSetupStep.WIRELESS_DEBUGGING -> Icons.Rounded.BugReport
+ SystemBridgeSetupStep.ADB_PAIRING -> Icons.Rounded.Link
+ SystemBridgeSetupStep.START_SERVICE -> Icons.Rounded.PlayArrow
+ SystemBridgeSetupStep.STARTED -> Icons.Rounded.CheckCircleOutline
+ }
+}
+
+@Composable
+private fun createPreviewStepContent(step: SystemBridgeSetupStep): StepContent {
+ val icon = getIconForStep(step)
return when (step) {
SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> StepContent(
title = stringResource(
- R.string.pro_mode_setup_wizard_enable_accessibility_service_title,
+ R.string.expert_mode_setup_wizard_enable_accessibility_service_title,
),
message = stringResource(
- R.string.pro_mode_setup_wizard_enable_accessibility_service_description,
+ R.string.expert_mode_setup_wizard_enable_accessibility_service_description,
),
- icon = Icons.Rounded.Accessibility,
+ icon = icon,
buttonText = stringResource(
- R.string.pro_mode_setup_wizard_enable_accessibility_service_button,
+ R.string.expert_mode_setup_wizard_enable_accessibility_service_button,
),
)
SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> StepContent(
title = stringResource(
- R.string.pro_mode_setup_wizard_enable_notification_permission_title,
+ R.string.expert_mode_setup_wizard_enable_notification_permission_title,
),
message = stringResource(
- R.string.pro_mode_setup_wizard_enable_notification_permission_description,
+ R.string.expert_mode_setup_wizard_enable_notification_permission_description,
),
- icon = Icons.Rounded.Notifications,
+ icon = icon,
buttonText = stringResource(
- R.string.pro_mode_setup_wizard_enable_notification_permission_button,
+ R.string.expert_mode_setup_wizard_enable_notification_permission_button,
),
)
SystemBridgeSetupStep.DEVELOPER_OPTIONS -> StepContent(
- title = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_title),
+ title = stringResource(
+ R.string.expert_mode_setup_wizard_enable_developer_options_title,
+ ),
message = stringResource(
- R.string.pro_mode_setup_wizard_enable_developer_options_description,
+ R.string.expert_mode_setup_wizard_enable_developer_options_description,
),
- icon = Icons.Rounded.Build,
- buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button),
+ icon = icon,
+ buttonText = stringResource(R.string.expert_mode_setup_wizard_go_to_settings_button),
)
SystemBridgeSetupStep.WIFI_NETWORK -> StepContent(
- title = stringResource(R.string.pro_mode_setup_wizard_connect_wifi_title),
- message = stringResource(R.string.pro_mode_setup_wizard_connect_wifi_description),
- icon = KeyMapperIcons.SignalWifiNotConnected,
- buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button),
+ title = stringResource(R.string.expert_mode_setup_wizard_connect_wifi_title),
+ message = stringResource(R.string.expert_mode_setup_wizard_connect_wifi_description),
+ icon = icon,
+ buttonText = stringResource(R.string.expert_mode_setup_wizard_go_to_settings_button),
)
SystemBridgeSetupStep.WIRELESS_DEBUGGING -> StepContent(
- title = stringResource(R.string.pro_mode_setup_wizard_enable_wireless_debugging_title),
+ title = stringResource(
+ R.string.expert_mode_setup_wizard_enable_wireless_debugging_title,
+ ),
message = stringResource(
- R.string.pro_mode_setup_wizard_enable_wireless_debugging_description,
+ R.string.expert_mode_setup_wizard_enable_wireless_debugging_description,
),
- icon = Icons.Rounded.BugReport,
- buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button),
+ icon = icon,
+ buttonText = stringResource(R.string.expert_mode_setup_wizard_go_to_settings_button),
)
SystemBridgeSetupStep.ADB_PAIRING -> StepContent(
- title = stringResource(R.string.pro_mode_setup_wizard_pair_wireless_debugging_title),
+ title = stringResource(R.string.expert_mode_setup_wizard_pair_wireless_debugging_title),
message = stringResource(
- R.string.pro_mode_setup_wizard_pair_wireless_debugging_description,
+ R.string.expert_mode_setup_wizard_pair_wireless_debugging_description,
),
- icon = Icons.Rounded.Link,
- buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button),
+ icon = icon,
+ buttonText = stringResource(R.string.expert_mode_setup_wizard_go_to_settings_button),
)
SystemBridgeSetupStep.START_SERVICE -> StepContent(
- title = stringResource(R.string.pro_mode_setup_wizard_start_service_title),
- message = stringResource(R.string.pro_mode_setup_wizard_start_service_description),
- icon = Icons.Rounded.PlayArrow,
- buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service),
+ title = stringResource(R.string.expert_mode_setup_wizard_start_service_title),
+ message = stringResource(R.string.expert_mode_setup_wizard_start_service_description),
+ icon = icon,
+ buttonText = stringResource(R.string.expert_mode_root_detected_button_start_service),
)
SystemBridgeSetupStep.STARTED -> StepContent(
- title = stringResource(R.string.pro_mode_setup_wizard_complete_title),
- message = stringResource(R.string.pro_mode_setup_wizard_complete_text),
- icon = Icons.Rounded.CheckCircleOutline,
- buttonText = stringResource(R.string.pro_mode_setup_wizard_complete_button),
+ title = stringResource(R.string.expert_mode_setup_wizard_complete_title),
+ message = stringResource(R.string.expert_mode_setup_wizard_complete_text),
+ icon = icon,
+ buttonText = stringResource(R.string.expert_mode_setup_wizard_complete_button),
)
}
}
-private data class StepContent(
- val title: String,
- val message: String,
- val icon: ImageVector,
- val buttonText: String,
-)
-
@Preview(name = "Accessibility Service Step")
@Composable
-private fun ProModeSetupScreenAccessibilityServicePreview() {
+private fun ExpertModeSetupScreenAccessibilityServicePreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ val step = SystemBridgeSetupStep.ACCESSIBILITY_SERVICE
+ ExpertModeSetupScreen(
state = State.Data(
- ProModeSetupState(
+ ExpertModeSetupState(
stepNumber = 1,
stepCount = 6,
- step = SystemBridgeSetupStep.ACCESSIBILITY_SERVICE,
+ step = step,
+ stepContent = createPreviewStepContent(step),
isSetupAssistantChecked = false,
isSetupAssistantButtonEnabled = false,
+ isStarting = false,
),
),
)
@@ -421,16 +465,19 @@ private fun ProModeSetupScreenAccessibilityServicePreview() {
@Preview(name = "Notification Permission Step")
@Composable
-private fun ProModeSetupScreenNotificationPermissionPreview() {
+private fun ExpertModeSetupScreenNotificationPermissionPreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ val step = SystemBridgeSetupStep.NOTIFICATION_PERMISSION
+ ExpertModeSetupScreen(
state = State.Data(
- ProModeSetupState(
+ ExpertModeSetupState(
stepNumber = 2,
stepCount = 6,
- step = SystemBridgeSetupStep.NOTIFICATION_PERMISSION,
+ step = step,
+ stepContent = createPreviewStepContent(step),
isSetupAssistantChecked = false,
isSetupAssistantButtonEnabled = true,
+ isStarting = false,
),
),
)
@@ -439,16 +486,19 @@ private fun ProModeSetupScreenNotificationPermissionPreview() {
@Preview(name = "Developer Options Step")
@Composable
-private fun ProModeSetupScreenDeveloperOptionsPreview() {
+private fun ExpertModeSetupScreenDeveloperOptionsPreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ val step = SystemBridgeSetupStep.DEVELOPER_OPTIONS
+ ExpertModeSetupScreen(
state = State.Data(
- ProModeSetupState(
+ ExpertModeSetupState(
stepNumber = 2,
stepCount = 6,
- step = SystemBridgeSetupStep.DEVELOPER_OPTIONS,
+ step = step,
+ stepContent = createPreviewStepContent(step),
isSetupAssistantChecked = false,
isSetupAssistantButtonEnabled = true,
+ isStarting = false,
),
),
)
@@ -457,16 +507,19 @@ private fun ProModeSetupScreenDeveloperOptionsPreview() {
@Preview(name = "WiFi Network Step")
@Composable
-private fun ProModeSetupScreenWifiNetworkPreview() {
+private fun ExpertModeSetupScreenWifiNetworkPreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ val step = SystemBridgeSetupStep.WIFI_NETWORK
+ ExpertModeSetupScreen(
state = State.Data(
- ProModeSetupState(
+ ExpertModeSetupState(
stepNumber = 3,
stepCount = 6,
- step = SystemBridgeSetupStep.WIFI_NETWORK,
+ step = step,
+ stepContent = createPreviewStepContent(step),
isSetupAssistantChecked = false,
isSetupAssistantButtonEnabled = true,
+ isStarting = false,
),
),
)
@@ -475,16 +528,19 @@ private fun ProModeSetupScreenWifiNetworkPreview() {
@Preview(name = "Wireless Debugging Step")
@Composable
-private fun ProModeSetupScreenWirelessDebuggingPreview() {
+private fun ExpertModeSetupScreenWirelessDebuggingPreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ val step = SystemBridgeSetupStep.WIRELESS_DEBUGGING
+ ExpertModeSetupScreen(
state = State.Data(
- ProModeSetupState(
+ ExpertModeSetupState(
stepNumber = 4,
stepCount = 6,
- step = SystemBridgeSetupStep.WIRELESS_DEBUGGING,
+ step = step,
+ stepContent = createPreviewStepContent(step),
isSetupAssistantChecked = false,
isSetupAssistantButtonEnabled = true,
+ isStarting = false,
),
),
)
@@ -493,16 +549,19 @@ private fun ProModeSetupScreenWirelessDebuggingPreview() {
@Preview(name = "ADB Pairing Step", widthDp = 400, heightDp = 400)
@Composable
-private fun ProModeSetupScreenAdbPairingPreview() {
+private fun ExpertModeSetupScreenAdbPairingPreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ val step = SystemBridgeSetupStep.ADB_PAIRING
+ ExpertModeSetupScreen(
state = State.Data(
- ProModeSetupState(
+ ExpertModeSetupState(
stepNumber = 5,
stepCount = 6,
- step = SystemBridgeSetupStep.ADB_PAIRING,
+ step = step,
+ stepContent = createPreviewStepContent(step),
isSetupAssistantChecked = true,
isSetupAssistantButtonEnabled = true,
+ isStarting = false,
),
),
)
@@ -511,16 +570,19 @@ private fun ProModeSetupScreenAdbPairingPreview() {
@Preview(name = "Start Service Step", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
-private fun ProModeSetupScreenStartServicePreview() {
+private fun ExpertModeSetupScreenStartServicePreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ val step = SystemBridgeSetupStep.START_SERVICE
+ ExpertModeSetupScreen(
state = State.Data(
- ProModeSetupState(
+ ExpertModeSetupState(
stepNumber = 6,
stepCount = 6,
- step = SystemBridgeSetupStep.START_SERVICE,
+ step = step,
+ stepContent = createPreviewStepContent(step),
isSetupAssistantChecked = true,
isSetupAssistantButtonEnabled = true,
+ isStarting = false,
),
),
)
@@ -529,16 +591,19 @@ private fun ProModeSetupScreenStartServicePreview() {
@Preview(name = "Started", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
-private fun ProModeSetupScreenStartedPreview() {
+private fun ExpertModeSetupScreenStartedPreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ val step = SystemBridgeSetupStep.STARTED
+ ExpertModeSetupScreen(
state = State.Data(
- ProModeSetupState(
+ ExpertModeSetupState(
stepNumber = 8,
stepCount = 8,
- step = SystemBridgeSetupStep.STARTED,
+ step = step,
+ stepContent = createPreviewStepContent(step),
isSetupAssistantChecked = true,
isSetupAssistantButtonEnabled = true,
+ isStarting = false,
),
),
)
@@ -547,9 +612,9 @@ private fun ProModeSetupScreenStartedPreview() {
@Preview(name = "Loading", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
-private fun ProModeSetupScreenLoadingPreview() {
+private fun ExpertModeSetupScreenLoadingPreview() {
KeyMapperTheme {
- ProModeSetupScreen(
+ ExpertModeSetupScreen(
state = State.Loading,
)
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupState.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupState.kt
new file mode 100644
index 0000000000..f920a4aad3
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupState.kt
@@ -0,0 +1,13 @@
+package io.github.sds100.keymapper.base.expertmode
+
+import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep
+
+data class ExpertModeSetupState(
+ val stepNumber: Int,
+ val stepCount: Int,
+ val step: SystemBridgeSetupStep,
+ val stepContent: StepContent,
+ val isSetupAssistantChecked: Boolean,
+ val isSetupAssistantButtonEnabled: Boolean,
+ val isStarting: Boolean,
+)
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupViewModel.kt
new file mode 100644
index 0000000000..bf204bdb93
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupViewModel.kt
@@ -0,0 +1,26 @@
+package io.github.sds100.keymapper.base.expertmode
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
+import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
+import javax.inject.Inject
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class ExpertModeSetupViewModel @Inject constructor(
+ delegate: SystemBridgeSetupDelegate,
+ navigationProvider: NavigationProvider,
+ resourceProvider: ResourceProvider,
+) : ViewModel(),
+ SystemBridgeSetupDelegate by delegate,
+ NavigationProvider by navigationProvider,
+ ResourceProvider by resourceProvider {
+
+ fun onBackClick() {
+ viewModelScope.launch {
+ popBackStack()
+ }
+ }
+}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt
similarity index 81%
rename from base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt
rename to base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt
index 415b7163f0..87dfc97116 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeViewModel.kt
@@ -1,4 +1,4 @@
-package io.github.sds100.keymapper.base.promode
+package io.github.sds100.keymapper.base.expertmode
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel
-class ProModeViewModel @Inject constructor(
+class ExpertModeViewModel @Inject constructor(
private val useCase: SystemBridgeSetupUseCase,
resourceProvider: ResourceProvider,
dialogProvider: DialogProvider,
@@ -43,23 +43,24 @@ class ProModeViewModel @Inject constructor(
}
@OptIn(ExperimentalCoroutinesApi::class)
- val warningState: StateFlow =
+ val warningState: StateFlow =
useCase.isWarningUnderstood
.flatMapLatest { isUnderstood -> createWarningStateFlow(isUnderstood) }
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
- ProModeWarningState.CountingDown(
+ ExpertModeWarningState.CountingDown(
WARNING_COUNT_DOWN_SECONDS,
),
)
- val setupState: StateFlow> =
+ val setupState: StateFlow> =
combine(
useCase.isSystemBridgeConnected,
useCase.isRootGranted,
useCase.shizukuSetupState,
useCase.isNotificationPermissionGranted,
+ useCase.isSystemBridgeStarting,
::buildSetupState,
).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading)
@@ -81,17 +82,17 @@ class ProModeViewModel @Inject constructor(
showInfoCard = true
}
- private fun createWarningStateFlow(isUnderstood: Boolean): Flow =
+ private fun createWarningStateFlow(isUnderstood: Boolean): Flow =
if (isUnderstood) {
- flowOf(ProModeWarningState.Understood)
+ flowOf(ExpertModeWarningState.Understood)
} else {
flow {
repeat(WARNING_COUNT_DOWN_SECONDS) {
- emit(ProModeWarningState.CountingDown(WARNING_COUNT_DOWN_SECONDS - it))
+ emit(ExpertModeWarningState.CountingDown(WARNING_COUNT_DOWN_SECONDS - it))
delay(1000L)
}
- emit(ProModeWarningState.Idle)
+ emit(ExpertModeWarningState.Idle)
}
}
@@ -138,7 +139,7 @@ class ProModeViewModel @Inject constructor(
fun onSetupWithKeyMapperClick() {
viewModelScope.launch {
- navigate("setup_pro_mode_with_key_mapper", NavDestination.ProModeSetup)
+ navigate("setup_expert_mode_with_key_mapper", NavDestination.ExpertModeSetup)
}
}
@@ -155,38 +156,41 @@ class ProModeViewModel @Inject constructor(
isRootGranted: Boolean,
shizukuSetupState: ShizukuSetupState,
isNotificationPermissionGranted: Boolean,
- ): State {
+ isSystemBridgeStarting: Boolean,
+ ): State {
if (isSystemBridgeConnected) {
return State.Data(
- ProModeState.Started(
+ ExpertModeState.Started(
isDefaultUsbModeCompatible =
useCase.isCompatibleUsbModeSelected().valueOrNull() ?: false,
),
)
} else {
return State.Data(
- ProModeState.Stopped(
+ ExpertModeState.Stopped(
isRootGranted = isRootGranted,
shizukuSetupState = shizukuSetupState,
isNotificationPermissionGranted = isNotificationPermissionGranted,
+ isStarting = isSystemBridgeStarting,
),
)
}
}
}
-sealed class ProModeWarningState {
- data class CountingDown(val seconds: Int) : ProModeWarningState()
- data object Idle : ProModeWarningState()
- data object Understood : ProModeWarningState()
+sealed class ExpertModeWarningState {
+ data class CountingDown(val seconds: Int) : ExpertModeWarningState()
+ data object Idle : ExpertModeWarningState()
+ data object Understood : ExpertModeWarningState()
}
-sealed class ProModeState {
+sealed class ExpertModeState {
data class Stopped(
val isRootGranted: Boolean,
val shizukuSetupState: ShizukuSetupState,
val isNotificationPermissionGranted: Boolean,
- ) : ProModeState()
+ val isStarting: Boolean,
+ ) : ExpertModeState()
- data class Started(val isDefaultUsbModeCompatible: Boolean) : ProModeState()
+ data class Started(val isDefaultUsbModeCompatible: Boolean) : ExpertModeState()
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ShizukuSetupState.kt
similarity index 66%
rename from base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt
rename to base/src/main/java/io/github/sds100/keymapper/base/expertmode/ShizukuSetupState.kt
index 1c9814a519..2f06044bc0 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ShizukuSetupState.kt
@@ -1,4 +1,4 @@
-package io.github.sds100.keymapper.base.promode
+package io.github.sds100.keymapper.base.expertmode
enum class ShizukuSetupState {
NOT_FOUND,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/StepContent.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/StepContent.kt
new file mode 100644
index 0000000000..8aac069b9b
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/StepContent.kt
@@ -0,0 +1,10 @@
+package io.github.sds100.keymapper.base.expertmode
+
+import androidx.compose.ui.graphics.vector.ImageVector
+
+data class StepContent(
+ val title: String,
+ val message: String,
+ val icon: ImageVector,
+ val buttonText: String,
+)
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt
similarity index 58%
rename from base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt
rename to base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt
index ca660a171c..cb69b61f41 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt
@@ -1,20 +1,22 @@
-package io.github.sds100.keymapper.base.promode
+package io.github.sds100.keymapper.base.expertmode
+import android.annotation.SuppressLint
import android.os.Build
-import android.os.SystemClock
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import io.github.sds100.keymapper.base.BaseMainActivity
+import io.github.sds100.keymapper.base.BuildConfig
import io.github.sds100.keymapper.base.R
import io.github.sds100.keymapper.base.system.notifications.NotificationController.Companion.CHANNEL_SETUP_ASSISTANT
import io.github.sds100.keymapper.base.system.notifications.NotificationController.Companion.ID_SYSTEM_BRIDGE_STATUS
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
+import io.github.sds100.keymapper.common.BuildConfigProvider
import io.github.sds100.keymapper.common.notifications.KMNotificationAction
+import io.github.sds100.keymapper.common.utils.Clock
import io.github.sds100.keymapper.common.utils.Constants
import io.github.sds100.keymapper.data.Keys
import io.github.sds100.keymapper.data.PreferenceDefaults
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
-import io.github.sds100.keymapper.sysbridge.BuildConfig
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
import io.github.sds100.keymapper.sysbridge.manager.isConnected
@@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
/**
@@ -54,6 +57,8 @@ import timber.log.Timber
@Singleton
class SystemBridgeAutoStarter @Inject constructor(
private val coroutineScope: CoroutineScope,
+ private val buildConfig: BuildConfigProvider,
+ private val clock: Clock,
private val suAdapter: SuAdapter,
private val shizukuAdapter: ShizukuAdapter,
private val connectionManager: SystemBridgeConnectionManager,
@@ -71,101 +76,107 @@ class SystemBridgeAutoStarter @Inject constructor(
}
// Use flatMapLatest so that any calls to ADB are only done if strictly necessary.
+ @SuppressLint("NewApi")
@OptIn(ExperimentalCoroutinesApi::class)
private val autoStartTypeFlow: Flow =
- suAdapter.isRootGranted.flatMapLatest { isRooted ->
- if (isRooted) {
- flowOf(AutoStartType.ROOT)
- } else {
- val useShizukuFlow =
- combine(
- shizukuAdapter.isStarted,
- permissionAdapter.isGrantedFlow(Permission.SHIZUKU),
- ) { isStarted, isGranted ->
- isStarted && isGranted
- }
-
- useShizukuFlow.flatMapLatest { useShizuku ->
- if (useShizuku) {
- flowOf(AutoStartType.SHIZUKU)
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- val isAdbAutoStartAllowed = combine(
- permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS),
- networkAdapter.isWifiConnected,
- ) { isWriteSecureSettingsGranted, isWifiConnected ->
- isWriteSecureSettingsGranted &&
- isWifiConnected &&
- setupController.isAdbPaired()
+ suAdapter.isRootGranted
+ .filterNotNull()
+ .flatMapLatest { isRooted ->
+ if (isRooted) {
+ flowOf(AutoStartType.ROOT)
+ } else {
+ 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(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()
+ }
+
+ isAdbAutoStartAllowed.distinctUntilChanged()
+ .map { isAdbAutoStartAllowed ->
+ if (isAdbAutoStartAllowed) {
+ AutoStartType.ADB
+ } else {
+ null
+ }
+ }.filterNotNull()
+ } else {
+ flowOf(null)
+ }
}
}
}
- }
/**
* This emits values when the system bridge needs restarting after it being killed.
*/
@OptIn(ExperimentalCoroutinesApi::class)
- private val restartFlow: 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.isExpected
+ connectionState.isStoppedByUser ||
+ !getIsUsedBefore() ||
+ getIsStoppedByUser() ||
+ isSystemBridgeEmergencyKilled() ||
+ !isAutoStartEnabled()
) {
flowOf(null)
- } else {
+ } else if (isWithinAutoStartCooldown()) {
// Do not autostart if the system bridge was killed shortly after.
// This prevents infinite loops happening.
- if (lastAutoStartTime != null &&
- connectionState.time - lastAutoStartTime!! < 30000
- ) {
- Timber.w(
- "Not auto starting the system bridge because it was last auto started less than 30 secs ago",
- )
- showSystemBridgeKilledNotification(
- getString(R.string.system_bridge_died_notification_not_restarting_text),
- )
- flowOf(null)
- } else {
- autoStartTypeFlow
- }
+ 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
}
}
- private var lastAutoStartTime: Long? = null
-
/**
* This must only be called once in the application lifecycle
*/
- @OptIn(FlowPreview::class)
+ @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
fun init() {
coroutineScope.launch {
- // The Key Mapper process may not necessarily be started on boot due to the
- // on boot receiver so assume if it is started within a minute of boot that
- // it should be auto started.
- val isBoot = SystemClock.uptimeMillis() < 60000
-
- if (isBoot) {
- handleAutoStartOnBoot()
- } else if (BuildConfig.DEBUG && connectionManager.isConnected()) {
+ Timber.i(
+ "SystemBridgeAutoStarter init: time since boot=${clock.elapsedRealtime() / 1000} seconds",
+ )
+
+ if (BuildConfig.DEBUG && connectionManager.isConnected()) {
+ delay(1000)
// This is useful when developing and need to restart the system bridge
// after making changes to it.
Timber.w("Restarting system bridge on debug build.")
connectionManager.restartSystemBridge()
- } else {
- handleAutoStartFromPreVersion4()
}
- // Only start collecting the restart flow after potentially auto starting it for the first time.
- restartFlow
+ // Wait 5 seconds for the system bridge to potentially connect itself to Key Mapper
+ // before deciding whether to start it.
+ delay(5000)
+
+ handleAutoStartFromPreVersion4()
+
+ autoStartFlow
.distinctUntilChanged() // Must come before the filterNotNull
.filterNotNull()
.collectLatest { type ->
@@ -174,74 +185,59 @@ class SystemBridgeAutoStarter @Inject constructor(
}
}
- private suspend fun handleAutoStartOnBoot() {
- // Do not autostart if the device was force rebooted. This may be a sign that PRO mode
- // was broken and the user was trying to reset it.
- val isCleanShutdown = preferences.get(Keys.isCleanShutdown).map { it ?: false }.first()
-
- Timber.i(
- "SystemBridgeAutoStarter init: isBoot=true, isCleanShutdown=$isCleanShutdown",
- )
-
- // Reset the value after reading it.
- preferences.set(Keys.isCleanShutdown, false)
-
- val isBootAutoStartEnabled = preferences.get(Keys.isProModeAutoStartBootEnabled)
- .map { it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT }
- .first()
-
- // Wait 5 seconds for the system bridge to potentially connect itself to Key Mapper
- // before starting it.
- delay(5000)
-
- val connectionState = connectionManager.connectionState.value
-
- if (isCleanShutdown &&
- isBootAutoStartEnabled &&
- connectionState !is SystemBridgeConnectionState.Connected
- ) {
- val autoStartType = autoStartTypeFlow.first()
+ private suspend fun handleAutoStartFromPreVersion4() {
+ @Suppress("DEPRECATION")
+ val upgradedFromPreVersion4 =
+ preferences.get(Keys.handledUpgradeToExpertMode).first() == null
- if (autoStartType != null) {
- autoStart(autoStartType)
- }
+ if (!upgradedFromPreVersion4) {
+ return
}
- }
- private suspend fun handleAutoStartFromPreVersion4() {
- val isFirstTime = preferences.get(Keys.handledRootToProModeUpgrade).first() == null
+ val isRooted: Boolean = withTimeoutOrNull(1000) {
+ suAdapter.isRootGranted.filterNotNull().first()
+ } ?: false
- if (isFirstTime && suAdapter.isRootGranted.value) {
+ if (isRooted) {
Timber.i(
"Auto starting system bridge because upgraded from pre version 4.0 and was rooted",
)
autoStart(AutoStartType.ROOT)
- preferences.set(Keys.handledRootToProModeUpgrade, true)
+ preferences.set(Keys.handledUpgradeToExpertMode, true)
+ preferences.set(Keys.keyEventActionsUseSystemBridge, true)
+ return
}
- }
- private suspend fun autoStart(type: AutoStartType) {
- if (isSystemBridgeEmergencyKilled()) {
- Timber.w(
- "Not auto starting the system bridge because it was emergency killed by the user",
+ // Try Shizuku after the root check because root is more reliable.
+ val isShizukuStarted: Boolean = shizukuAdapter.isStarted.value
+
+ if (isShizukuStarted) {
+ Timber.i(
+ "Auto starting system bridge because upgraded from pre version 4.0 and Shizuku was started",
)
+
+ autoStart(AutoStartType.SHIZUKU)
+ preferences.set(Keys.handledUpgradeToExpertMode, true)
+ preferences.set(Keys.keyEventActionsUseSystemBridge, true)
return
}
+ }
+ private suspend fun autoStart(type: AutoStartType) {
if (connectionManager.isConnected()) {
Timber.i("Not auto starting with $type because already connected.")
return
}
- lastAutoStartTime = SystemClock.elapsedRealtime()
+ preferences.set(Keys.systemBridgeLastAutoStartTime, clock.elapsedRealtime())
when (type) {
AutoStartType.ADB -> {
Timber.i("Auto starting system bridge with ADB")
showAutoStartNotification(
getString(
- R.string.pro_mode_setup_notification_auto_start_system_bridge_adb_text,
+ R.string.expert_mode_setup_notification_auto_start_system_bridge_adb_text,
),
)
@@ -252,7 +248,7 @@ class SystemBridgeAutoStarter @Inject constructor(
Timber.i("Auto starting system bridge with Shizuku")
showAutoStartNotification(
getString(
- R.string.pro_mode_setup_notification_auto_start_system_bridge_shizuku_text,
+ R.string.expert_mode_setup_notification_auto_start_system_bridge_shizuku,
),
)
connectionManager.startWithShizuku()
@@ -262,7 +258,7 @@ class SystemBridgeAutoStarter @Inject constructor(
Timber.i("Auto starting system bridge with root")
showAutoStartNotification(
getString(
- R.string.pro_mode_setup_notification_auto_start_system_bridge_root_text,
+ R.string.expert_mode_setup_notification_auto_start_system_bridge_root_text,
),
)
connectionManager.startWithRoot()
@@ -281,17 +277,41 @@ class SystemBridgeAutoStarter @Inject constructor(
}
}
+ private suspend fun getIsUsedBefore(): Boolean {
+ return preferences.get(Keys.isSystemBridgeUsed).first() ?: false
+ }
+
+ private suspend fun getIsStoppedByUser(): Boolean {
+ return preferences.get(Keys.isSystemBridgeStoppedByUser).first() ?: false
+ }
+
private suspend fun isSystemBridgeEmergencyKilled(): Boolean {
return preferences.get(Keys.isSystemBridgeEmergencyKilled).first() == true
}
+ /**
+ * Whether the system bridge died less than 5 minutes after the previous time it was
+ * auto started.
+ */
+ private suspend fun isWithinAutoStartCooldown(): Boolean {
+ val lastAutoStartTime = preferences.get(Keys.systemBridgeLastAutoStartTime).first()
+ return lastAutoStartTime != null &&
+ clock.elapsedRealtime() - lastAutoStartTime < (5 * 60_000)
+ }
+
+ private suspend fun isAutoStartEnabled(): Boolean {
+ return preferences.get(Keys.isSystemBridgeKeepAliveEnabled)
+ .map { it ?: PreferenceDefaults.EXPERT_MODE_KEEP_ALIVE }
+ .first()
+ }
+
private fun showSystemBridgeKilledNotification(text: String) {
val model = NotificationModel(
id = ID_SYSTEM_BRIDGE_STATUS,
channel = CHANNEL_SETUP_ASSISTANT,
title = getString(R.string.system_bridge_died_notification_title),
text = text,
- icon = R.drawable.pro_mode,
+ icon = R.drawable.offline_bolt_24px,
showOnLockscreen = true,
onGoing = false,
priority = NotificationCompat.PRIORITY_MAX,
@@ -307,10 +327,12 @@ class SystemBridgeAutoStarter @Inject constructor(
private fun showAutoStartNotification(text: String) {
val model = NotificationModel(
id = ID_SYSTEM_BRIDGE_STATUS,
- title = getString(R.string.pro_mode_setup_notification_auto_start_system_bridge_title),
+ title = getString(
+ R.string.expert_mode_setup_notification_auto_start_system_bridge_title,
+ ),
text = text,
channel = CHANNEL_SETUP_ASSISTANT,
- icon = R.drawable.pro_mode,
+ icon = R.drawable.offline_bolt_24px,
priority = NotificationCompat.PRIORITY_MAX,
onGoing = true,
showIndeterminateProgress = true,
@@ -324,11 +346,13 @@ class SystemBridgeAutoStarter @Inject constructor(
val model = NotificationModel(
id = ID_SYSTEM_BRIDGE_STATUS,
title = getString(
- R.string.pro_mode_setup_notification_start_system_bridge_failed_title,
+ R.string.expert_mode_setup_notification_start_system_bridge_failed_title,
+ ),
+ text = getString(
+ R.string.expert_mode_setup_notification_start_system_bridge_failed_text,
),
- text = getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_text),
channel = CHANNEL_SETUP_ASSISTANT,
- icon = R.drawable.pro_mode,
+ icon = R.drawable.offline_bolt_24px,
onGoing = false,
showOnLockscreen = false,
autoCancel = true,
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt
similarity index 85%
rename from base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt
rename to base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt
index 464524777f..0589a80761 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt
@@ -1,4 +1,4 @@
-package io.github.sds100.keymapper.base.promode
+package io.github.sds100.keymapper.base.expertmode
import android.app.ActivityManager
import android.os.Build
@@ -25,23 +25,20 @@ import io.github.sds100.keymapper.data.Keys
import io.github.sds100.keymapper.data.PreferenceDefaults
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
-import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
+import io.github.sds100.keymapper.sysbridge.manager.awaitConnected
import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController
import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep
import io.github.sds100.keymapper.system.notifications.NotificationModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
@Suppress("KotlinConstantConditions")
@@ -88,12 +85,12 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor(
private val activityManager: ActivityManager = accessibilityService.getSystemService()!!
private val isInteractive: StateFlow =
- preferenceRepository.get(Keys.isProModeInteractiveSetupAssistantEnabled)
- .map { it ?: PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT }
+ preferenceRepository.get(Keys.isExpertModeInteractiveSetupAssistantEnabled)
+ .map { it ?: PreferenceDefaults.EXPERT_MODE_INTERACTIVE_SETUP_ASSISTANT }
.stateIn(
coroutineScope,
SharingStarted.Eagerly,
- PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT,
+ PreferenceDefaults.EXPERT_MODE_INTERACTIVE_SETUP_ASSISTANT,
)
private var interactionStep: InteractionStep? = null
@@ -206,11 +203,13 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor(
stopInteracting()
showNotification(
- getString(R.string.pro_mode_setup_notification_invalid_pairing_code_title),
- getString(R.string.pro_mode_setup_notification_invalid_pairing_code_text),
+ getString(R.string.expert_mode_setup_notification_invalid_pairing_code_title),
+ getString(R.string.expert_mode_setup_notification_invalid_pairing_code_text),
actions = listOf(
KMNotificationAction.RemoteInput.PairingCode to
- getString(R.string.pro_mode_setup_notification_action_input_pairing_code),
+ getString(
+ R.string.expert_mode_setup_notification_action_input_pairing_code,
+ ),
),
)
}
@@ -219,17 +218,9 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor(
private suspend fun onPairingSuccess() {
setupController.startWithAdb()
- val isStarted = try {
- withTimeout(10000L) {
- systemBridgeConnectionManager.connectionState
- .filterIsInstance()
- .first()
- }
-
- true
- } catch (_: TimeoutCancellationException) {
- false
- }
+ val isStarted = withTimeoutOrNull(10000L) {
+ systemBridgeConnectionManager.awaitConnected()
+ } != null
if (isStarted) {
Timber.i("System bridge started after pairing. Going back to Key Mapper.")
@@ -237,8 +228,8 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor(
} else {
Timber.e("Failed to start system bridge after pairing.")
showNotification(
- getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_title),
- getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_text),
+ getString(R.string.expert_mode_setup_notification_start_system_bridge_failed_title),
+ getString(R.string.expert_mode_setup_notification_start_system_bridge_failed_text),
onClickAction = KMNotificationAction.Activity.MainActivity(
BaseMainActivity.ACTION_START_SYSTEM_BRIDGE,
),
@@ -281,7 +272,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor(
channel = NotificationController.Companion.CHANNEL_SETUP_ASSISTANT,
title = title,
text = text,
- icon = R.drawable.pro_mode,
+ icon = R.drawable.offline_bolt_24px,
onGoing = false,
showOnLockscreen = false,
autoCancel = true,
@@ -309,15 +300,15 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor(
when (step) {
SystemBridgeSetupStep.DEVELOPER_OPTIONS -> {
showNotification(
- getString(R.string.pro_mode_setup_notification_tap_build_number_title),
- getString(R.string.pro_mode_setup_notification_tap_build_number_text),
+ getString(R.string.expert_mode_setup_notification_tap_build_number_title),
+ getString(R.string.expert_mode_setup_notification_tap_build_number_text),
)
}
SystemBridgeSetupStep.ADB_PAIRING -> {
showNotification(
- getString(R.string.pro_mode_setup_notification_pairing_title),
- getString(R.string.pro_mode_setup_notification_pairing_text),
+ getString(R.string.expert_mode_setup_notification_pairing_title),
+ getString(R.string.expert_mode_setup_notification_pairing_text),
)
interactionStep = InteractionStep.PAIR_DEVICE
@@ -339,15 +330,15 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor(
showNotification(
title = getString(
- R.string.pro_mode_setup_notification_pairing_button_not_found_title,
+ R.string.expert_mode_setup_notification_pairing_button_not_found_title,
),
text = getString(
- R.string.pro_mode_setup_notification_pairing_button_not_found_text,
+ R.string.expert_mode_setup_notification_pairing_button_not_found_text,
),
actions = listOf(
KMNotificationAction.RemoteInput.PairingCode to
getString(
- R.string.pro_mode_setup_notification_action_input_pairing_code,
+ R.string.expert_mode_setup_notification_action_input_pairing_code,
),
),
)
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupDelegate.kt
new file mode 100644
index 0000000000..c4ba397200
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupDelegate.kt
@@ -0,0 +1,209 @@
+package io.github.sds100.keymapper.base.expertmode
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Accessibility
+import androidx.compose.material.icons.rounded.BugReport
+import androidx.compose.material.icons.rounded.Build
+import androidx.compose.material.icons.rounded.CheckCircleOutline
+import androidx.compose.material.icons.rounded.Link
+import androidx.compose.material.icons.rounded.Notifications
+import androidx.compose.material.icons.rounded.PlayArrow
+import io.github.sds100.keymapper.base.R
+import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
+import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons
+import io.github.sds100.keymapper.base.utils.ui.compose.icons.SignalWifiNotConnected
+import io.github.sds100.keymapper.common.utils.State
+import io.github.sds100.keymapper.common.utils.dataOrNull
+import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+
+abstract class SystemBridgeSetupDelegateImpl(
+ val viewModelScope: CoroutineScope,
+ private val useCase: SystemBridgeSetupUseCase,
+ private val resourceProvider: ResourceProvider,
+) : SystemBridgeSetupDelegate,
+ ResourceProvider by resourceProvider {
+ override val setupState: StateFlow> =
+ combine(
+ useCase.nextSetupStep,
+ useCase.isSetupAssistantEnabled,
+ useCase.isSystemBridgeStarting,
+ ::buildState,
+ ).stateIn(
+ viewModelScope,
+ SharingStarted.Eagerly,
+ State.Loading,
+ )
+
+ override fun onSetupStepButtonClick() {
+ // Do not check the latest value in the use case because there is significant latency
+ // when it is checking whether it is paired
+ val currentStep = setupState.value.dataOrNull()?.step ?: return
+
+ when (currentStep) {
+ SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> useCase.enableAccessibilityService()
+ SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> useCase.requestNotificationPermission()
+ SystemBridgeSetupStep.DEVELOPER_OPTIONS -> useCase.enableDeveloperOptions()
+ SystemBridgeSetupStep.WIFI_NETWORK -> useCase.connectWifiNetwork()
+ SystemBridgeSetupStep.WIRELESS_DEBUGGING -> useCase.enableWirelessDebugging()
+ SystemBridgeSetupStep.ADB_PAIRING -> useCase.pairWirelessAdb()
+ SystemBridgeSetupStep.START_SERVICE -> useCase.startSystemBridgeWithAdb()
+ SystemBridgeSetupStep.STARTED -> onFinishClick()
+ }
+ }
+
+ abstract fun onFinishClick()
+
+ override fun onSetupAssistantClick() {
+ useCase.toggleSetupAssistant()
+ }
+
+ override fun getStepContent(step: SystemBridgeSetupStep): StepContent {
+ return when (step) {
+ SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> StepContent(
+ title = getString(
+ R.string.expert_mode_setup_wizard_enable_accessibility_service_title,
+ ),
+ message = getString(
+ R.string.expert_mode_setup_wizard_enable_accessibility_service_description,
+ ),
+ icon = Icons.Rounded.Accessibility,
+ buttonText = getString(
+ R.string.expert_mode_setup_wizard_enable_accessibility_service_button,
+ ),
+ )
+
+ SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> StepContent(
+ title = getString(
+ R.string.expert_mode_setup_wizard_enable_notification_permission_title,
+ ),
+ message = getString(
+ R.string.expert_mode_setup_wizard_enable_notification_permission_description,
+ ),
+ icon = Icons.Rounded.Notifications,
+ buttonText = getString(
+ R.string.expert_mode_setup_wizard_enable_notification_permission_button,
+ ),
+ )
+
+ SystemBridgeSetupStep.DEVELOPER_OPTIONS -> StepContent(
+ title = getString(
+ R.string.expert_mode_setup_wizard_enable_developer_options_title,
+ ),
+ message = getString(
+ R.string.expert_mode_setup_wizard_enable_developer_options_description,
+ ),
+ icon = Icons.Rounded.Build,
+ buttonText = getString(
+ R.string.expert_mode_setup_wizard_go_to_settings_button,
+ ),
+ )
+
+ SystemBridgeSetupStep.WIFI_NETWORK -> StepContent(
+ title = getString(
+ R.string.expert_mode_setup_wizard_connect_wifi_title,
+ ),
+ message = getString(
+ R.string.expert_mode_setup_wizard_connect_wifi_description,
+ ),
+ icon = KeyMapperIcons.SignalWifiNotConnected,
+ buttonText = getString(
+ R.string.expert_mode_setup_wizard_go_to_settings_button,
+ ),
+ )
+
+ SystemBridgeSetupStep.WIRELESS_DEBUGGING -> StepContent(
+ title = getString(
+ R.string.expert_mode_setup_wizard_enable_wireless_debugging_title,
+ ),
+ message = getString(
+ R.string.expert_mode_setup_wizard_enable_wireless_debugging_description,
+ ),
+ icon = Icons.Rounded.BugReport,
+ buttonText = getString(
+ R.string.expert_mode_setup_wizard_go_to_settings_button,
+ ),
+ )
+
+ SystemBridgeSetupStep.ADB_PAIRING -> StepContent(
+ title = getString(
+ R.string.expert_mode_setup_wizard_pair_wireless_debugging_title,
+ ),
+ message = getString(
+ R.string.expert_mode_setup_wizard_pair_wireless_debugging_description,
+ ),
+ icon = Icons.Rounded.Link,
+ buttonText = getString(
+ R.string.expert_mode_setup_wizard_go_to_settings_button,
+ ),
+ )
+
+ SystemBridgeSetupStep.START_SERVICE -> StepContent(
+ title = getString(
+ R.string.expert_mode_setup_wizard_start_service_title,
+ ),
+ message = getString(
+ R.string.expert_mode_setup_wizard_start_service_description,
+ ),
+ icon = Icons.Rounded.PlayArrow,
+ buttonText = getString(
+ R.string.expert_mode_root_detected_button_start_service,
+ ),
+ )
+
+ SystemBridgeSetupStep.STARTED -> StepContent(
+ title = getString(
+ R.string.expert_mode_setup_wizard_complete_title,
+ ),
+ message = getString(
+ R.string.expert_mode_setup_wizard_complete_text,
+ ),
+ icon = Icons.Rounded.CheckCircleOutline,
+ buttonText = getString(
+ R.string.expert_mode_setup_wizard_complete_button,
+ ),
+ )
+ }
+ }
+
+ private fun buildState(
+ step: SystemBridgeSetupStep,
+ isSetupAssistantUserEnabled: Boolean,
+ isStarting: Boolean,
+ ): State.Data {
+ // Uncheck the setup assistant if the accessibility service is disabled since it is
+ // required for the setup assistant to work
+ val isSetupAssistantChecked = if (step == SystemBridgeSetupStep.ACCESSIBILITY_SERVICE) {
+ false
+ } else {
+ isSetupAssistantUserEnabled
+ }
+
+ val stepContent = getStepContent(step)
+
+ return State.Data(
+ ExpertModeSetupState(
+ stepNumber = step.stepIndex + 1,
+ stepCount = SystemBridgeSetupStep.entries.size,
+ step = step,
+ stepContent = stepContent,
+ isSetupAssistantChecked = isSetupAssistantChecked,
+ isSetupAssistantButtonEnabled =
+ step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE &&
+ step != SystemBridgeSetupStep.STARTED,
+ isStarting = isStarting,
+ ),
+ )
+ }
+}
+
+interface SystemBridgeSetupDelegate {
+ val setupState: StateFlow>
+ fun onSetupStepButtonClick()
+ fun onSetupAssistantClick()
+ fun getStepContent(step: SystemBridgeSetupStep): StepContent
+}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt
similarity index 83%
rename from base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt
rename to base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt
index 570da2409a..c8ea53a633 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt
@@ -1,4 +1,4 @@
-package io.github.sds100.keymapper.base.promode
+package io.github.sds100.keymapper.base.expertmode
import android.os.Build
import android.os.Process
@@ -26,7 +26,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
@@ -54,7 +53,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
}
override val isWarningUnderstood: Flow =
- preferences.get(Keys.isProModeWarningUnderstood).map { it ?: false }
+ preferences.get(Keys.isExpertModeWarningUnderstood).map { it ?: false }
private val isAdbAutoStartAllowed: Flow =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -71,18 +70,18 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
}
override fun onUnderstoodWarning() {
- preferences.set(Keys.isProModeWarningUnderstood, true)
+ preferences.set(Keys.isExpertModeWarningUnderstood, true)
}
override val isSetupAssistantEnabled: Flow =
- preferences.get(Keys.isProModeInteractiveSetupAssistantEnabled).map {
- it ?: PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT
+ preferences.get(Keys.isExpertModeInteractiveSetupAssistantEnabled).map {
+ it ?: PreferenceDefaults.EXPERT_MODE_INTERACTIVE_SETUP_ASSISTANT
}
override fun toggleSetupAssistant() {
- preferences.update(Keys.isProModeInteractiveSetupAssistantEnabled) {
+ preferences.update(Keys.isExpertModeInteractiveSetupAssistantEnabled) {
if (it == null) {
- !PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT
+ !PreferenceDefaults.EXPERT_MODE_INTERACTIVE_SETUP_ASSISTANT
} else {
!it
}
@@ -93,6 +92,9 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
systemBridgeConnectionManager.connectionState
.map { it is SystemBridgeConnectionState.Connected }
+ override val isSystemBridgeStarting: Flow =
+ systemBridgeSetupController.isStarting
+
override val isNotificationPermissionGranted: Flow =
permissionAdapter.isGrantedFlow(Permission.POST_NOTIFICATIONS)
@@ -120,7 +122,7 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
}
}
- override val isRootGranted: Flow = suAdapter.isRootGranted
+ override val isRootGranted: Flow = suAdapter.isRootGranted.map { it ?: false }
override val shizukuSetupState: Flow = combine(
shizukuAdapter.isInstalled,
@@ -148,6 +150,10 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
}
override fun stopSystemBridge() {
+ // Save that they've stopped the system bridge so when the app process launches again
+ // it will set the isStoppedByUser to true.
+ preferences.set(Keys.isSystemBridgeStoppedByUser, true)
+
systemBridgeConnectionManager.stopSystemBridge()
}
@@ -174,38 +180,43 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
override fun startSystemBridgeWithRoot() {
preferences.set(Keys.isSystemBridgeEmergencyKilled, false)
+ preferences.set(Keys.isSystemBridgeStoppedByUser, false)
systemBridgeSetupController.startWithRoot()
}
override fun startSystemBridgeWithShizuku() {
preferences.set(Keys.isSystemBridgeEmergencyKilled, false)
+ preferences.set(Keys.isSystemBridgeStoppedByUser, false)
systemBridgeSetupController.startWithShizuku()
}
- override suspend fun startSystemBridgeWithAdb() {
+ override fun startSystemBridgeWithAdb() {
preferences.set(Keys.isSystemBridgeEmergencyKilled, false)
- if (isAdbAutoStartAllowed.first()) {
- systemBridgeSetupController.autoStartWithAdb()
- } else {
- systemBridgeSetupController.startWithAdb()
- }
+ preferences.set(Keys.isSystemBridgeStoppedByUser, false)
+ systemBridgeSetupController.startWithAdb()
+ }
+
+ override fun autoStartSystemBridgeWithAdb() {
+ preferences.set(Keys.isSystemBridgeEmergencyKilled, false)
+ preferences.set(Keys.isSystemBridgeStoppedByUser, false)
+ systemBridgeSetupController.autoStartWithAdb()
}
override fun isInfoDismissed(): Boolean {
- return preferences.get(Keys.isProModeInfoDismissed).map { it ?: false }.firstBlocking()
+ return preferences.get(Keys.isExpertModeInfoDismissed).map { it ?: false }.firstBlocking()
}
override fun dismissInfo() {
- preferences.set(Keys.isProModeInfoDismissed, true)
+ preferences.set(Keys.isExpertModeInfoDismissed, true)
}
override val isAutoStartBootEnabled: Flow =
- preferences.get(Keys.isProModeAutoStartBootEnabled)
- .map { it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT }
+ preferences.get(Keys.isSystemBridgeKeepAliveEnabled)
+ .map { it ?: PreferenceDefaults.EXPERT_MODE_KEEP_ALIVE }
override fun toggleAutoStartBoot() {
- preferences.update(Keys.isProModeAutoStartBootEnabled) {
- !(it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT)
+ preferences.update(Keys.isSystemBridgeKeepAliveEnabled) {
+ !(it ?: PreferenceDefaults.EXPERT_MODE_KEEP_ALIVE)
}
}
@@ -238,11 +249,17 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor(
return when {
accessibilityServiceState != AccessibilityServiceState.ENABLED ->
SystemBridgeSetupStep.ACCESSIBILITY_SERVICE
+
!isNotificationPermissionGranted -> SystemBridgeSetupStep.NOTIFICATION_PERMISSION
+
!isDeveloperOptionsEnabled -> SystemBridgeSetupStep.DEVELOPER_OPTIONS
+
!isWifiConnected -> SystemBridgeSetupStep.WIFI_NETWORK
+
!isWirelessDebuggingEnabled -> SystemBridgeSetupStep.WIRELESS_DEBUGGING
+
isWirelessDebuggingEnabled -> SystemBridgeSetupStep.ADB_PAIRING
+
else -> SystemBridgeSetupStep.START_SERVICE
}
}
@@ -262,6 +279,7 @@ interface SystemBridgeSetupUseCase {
fun toggleSetupAssistant()
val isSystemBridgeConnected: Flow
+ val isSystemBridgeStarting: Flow
val nextSetupStep: Flow
val isRootGranted: Flow
@@ -281,7 +299,8 @@ interface SystemBridgeSetupUseCase {
fun pairWirelessAdb()
fun startSystemBridgeWithRoot()
fun startSystemBridgeWithShizuku()
- suspend fun startSystemBridgeWithAdb()
+ fun startSystemBridgeWithAdb()
+ fun autoStartSystemBridgeWithAdb()
fun isCompatibleUsbModeSelected(): KMResult
}
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt
index 4118c107f2..c5b71421a0 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt
@@ -135,11 +135,11 @@ fun HomeKeyMapListScreen(
sheetState = sheetState,
onDismissRequest = viewModel::dismissFixKeyEventActionBottomSheet,
onEnableAccessibilityServiceClick = viewModel::onEnableAccessibilityServiceClick,
- onEnableProModeClick = viewModel::onEnableProModeForKeyEventActionsClick,
+ onEnableExpertModeClick = viewModel::onEnableExpertModeForKeyEventActionsClick,
onEnableInputMethodClick = viewModel::onEnableImeClick,
onChooseInputMethodClick = viewModel::onChooseImeClick,
onDoneClick = viewModel::dismissFixKeyEventActionBottomSheet,
- onSelectProMode = viewModel::onSelectProMode,
+ onSelectExpertMode = viewModel::onSelectExpertMode,
onSelectInputMethod = viewModel::onSelectInputMethod,
onAutoSwitchImeCheckedChange = viewModel::onAutoSwitchImeCheckedChange,
)
diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt
index 111593b800..4e85c46321 100644
--- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt
+++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt
@@ -336,9 +336,7 @@ class KeyMapListViewModel(
Triple(listState, appBarState, showCreateKeyMapTapTarget)
}.collectLatest { (listState, appBarState, showCreateKeyMapTapTarget) ->
listState.ifIsData { list ->
- if (list.isNotEmpty()) {
- showFabText = false
- }
+ showFabText = list.isEmpty()
}
_state.value =
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
new file mode 100644
index 0000000000..9dc7ea17d5
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevDevicesDelegate.kt
@@ -0,0 +1,120 @@
+package io.github.sds100.keymapper.base.input
+
+import androidx.annotation.RequiresApi
+import io.github.sds100.keymapper.common.models.EvdevDeviceInfo
+import io.github.sds100.keymapper.common.models.GrabTargetKeyCode
+import io.github.sds100.keymapper.common.models.GrabbedDeviceHandle
+import io.github.sds100.keymapper.common.utils.Constants
+import io.github.sds100.keymapper.common.utils.onFailure
+import io.github.sds100.keymapper.common.utils.valueIfFailure
+import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
+import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+
+/**
+ * Need to use a cache that maps a device id to the other device information. This information
+ * could be sent in the onEvdevEvent callback instead, but sending non-primitive strings for the
+ * device name introduces extra overhead across Binder and JNI.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@RequiresApi(Constants.SYSTEM_BRIDGE_MIN_API)
+@Singleton
+class EvdevDevicesDelegate @Inject constructor(
+ private val coroutineScope: CoroutineScope,
+ private val systemBridgeConnectionManager: SystemBridgeConnectionManager,
+) {
+ private val grabbedDevicesById: MutableStateFlowUse o código de chave %dUse o código de digitalização %dNenhum código de digitalização salvo
- Use o modo PRO
+ Use o modo PROAdicionar maisRemover
@@ -1276,85 +1276,85 @@ Deixe em branco se alguma rede Wi-Fi precisar ser correspondida.+%d restrições herdadas
- Modo PRO
- Importante!
- Remapear botões no modo PRO é perigoso e pode fazer com que eles parem de funcionar se você os remapear incorretamente.\n\nSe você cometer um erro, pode ser necessário reiniciar o dispositivo à força segurando os botões de ligar e volume por 30 segundos — consulte o manual do seu dispositivo ou a internet para saber como fazer isso.
- %d…
- Eu entendo
- Entendido
- Configurar
- Root detectado
- Você pode pular o processo de configuração concedendo permissão de root ao Key Mapper. Isso permitirá que o Key Mapper inicie automaticamente o modo PRO na inicialização.
- Iniciar modo PRO
- Shizuku detectada
- Você pode pular o processo de configuração dando permissão ao Key Mapper Shizuku.
- Iniciar Shizuku
- Solicitar permissão
- Iniciar modo PRO
- Configurar com o Key Mapper
- Continuar
- Continuar (Android 11+)
- Opções
- Habilitar modo PRO para todos os mapas principais
- O Key Mapper usará o ADB Shell para remapeamento
- Essas configurações ficarão indisponíveis até que você confirme o aviso.
- O serviço do modo PRO está em execução
- Parar
- Iniciar automaticamente na inicialização
- O Modo PRO será iniciado sempre que você ligar ou reiniciar seu dispositivo
- Dica de emergência
- Se o botão liga/desliga parar de funcionar, mantenha-o pressionado por 10 segundos e solte para desativar o Modo PRO.
- Assistente de configuração
- Etapa %d de %d
- Use o assistente de configuração interativo
- Interagir automaticamente com as configurações
- Habilitar primeiro o serviço de acessibilidade
- Assista ao tutorial
- Iniciar serviço
- Vá para as configurações
- Iniciar serviço
- Ativar serviço de acessibilidade
- O Key Mapper usa um serviço para ajudar você a configurar o modo PRO. Ele também é útil para mapeamentos de teclas comuns.
- Habilitar opções do desenvolvedor
- O Key Mapper precisa usar o Android Debug Bridge para iniciar o modo PRO, e você precisa habilitar as opções do desenvolvedor para isso.
- Conectar a uma rede WiFi
- O Key Mapper precisa de uma rede Wi-Fi para habilitar o ADB. Você não precisa de conexão com a internet.\n\nSem rede Wi-Fi? Use um ponto de acesso do celular de outra pessoa.
- Habilitar depuração sem fio
- O Key Mapper usa depuração sem fio para iniciar seu serviço de remapeamento e entrada.
- Emparelhar depuração sem fio
- O Key Mapper precisa ser pareado com a depuração sem fio antes de poder iniciar seu serviço de remapeamento e entrada.
- Iniciar serviço
- O Key Mapper precisa se conectar ao Android Debug Bridge para iniciar o serviço do modo PRO.
- Permitir notificações
- O Key Mapper precisa de permissão para notificá-lo caso haja algum problema com o processo de configuração.
- Dê permissão
- Assistente de configuração
- O modo PRO está em execução
- Agora você pode remapear botões quando a tela estiver desligada e usar mais ações.
- Terminar
- Habilitar opções do desenvolvedor
- Toque repetidamente no número da compilação
- Emparelhamento automático
- Procurando código de pareamento e porta…
- Não é possível encontrar a porta e o código de emparelhamento
- Toque no botão para parear com o código de pareamento e digite o código aqui
- Falha ao iniciar o modo PRO
- Toque para configurar novamente. Tente o pareamento ADB e reinicie o telefone se o problema persistir.
- Modo PRO de inicialização automática
- Usando root
- Usando shizuku
- Usando ADB via WiFi
- Modo PRO iniciado
- Divirta-se remapeando! ❤️
- Falha no emparelhamento
- Mantenha o pop-up do código de emparelhamento na tela ao enviar o código de emparelhamento
- Código de pareamento de entrada
- O que posso fazer com o modo PRO?
- 📲 Você pode remapear mais botões, como o botão de energia.
+ Modo PRO
+ Importante!
+ Remapear botões no modo PRO é perigoso e pode fazer com que eles parem de funcionar se você os remapear incorretamente.\n\nSe você cometer um erro, pode ser necessário reiniciar o dispositivo à força segurando os botões de ligar e volume por 30 segundos — consulte o manual do seu dispositivo ou a internet para saber como fazer isso.
+ %d…
+ Eu entendo
+ Entendido
+ Configurar
+ Root detectado
+ Você pode pular o processo de configuração concedendo permissão de root ao Key Mapper. Isso permitirá que o Key Mapper inicie automaticamente o modo PRO na inicialização.
+ Iniciar modo PRO
+ Shizuku detectada
+ Você pode pular o processo de configuração dando permissão ao Key Mapper Shizuku.
+ Iniciar Shizuku
+ Solicitar permissão
+ Iniciar modo PRO
+ Configurar com o Key Mapper
+ Continuar
+ Continuar (Android 11+)
+ Opções
+ Habilitar modo PRO para todos os mapas principais
+ O Key Mapper usará o ADB Shell para remapeamento
+ Essas configurações ficarão indisponíveis até que você confirme o aviso.
+ O serviço do modo PRO está em execução
+ Parar
+ Iniciar automaticamente na inicialização
+ O Modo PRO será iniciado sempre que você ligar ou reiniciar seu dispositivo
+ Dica de emergência
+ Se o botão liga/desliga parar de funcionar, mantenha-o pressionado por 10 segundos e solte para desativar o Modo PRO.
+ Assistente de configuração
+ Etapa %d de %d
+ Use o assistente de configuração interativo
+ Interagir automaticamente com as configurações
+ Habilitar primeiro o serviço de acessibilidade
+ Assista ao tutorial
+ Iniciar serviço
+ Vá para as configurações
+ Iniciar serviço
+ Ativar serviço de acessibilidade
+ O Key Mapper usa um serviço para ajudar você a configurar o modo PRO. Ele também é útil para mapeamentos de teclas comuns.
+ Habilitar opções do desenvolvedor
+ O Key Mapper precisa usar o Android Debug Bridge para iniciar o modo PRO, e você precisa habilitar as opções do desenvolvedor para isso.
+ Conectar a uma rede WiFi
+ O Key Mapper precisa de uma rede Wi-Fi para habilitar o ADB. Você não precisa de conexão com a internet.\n\nSem rede Wi-Fi? Use um ponto de acesso do celular de outra pessoa.
+ Habilitar depuração sem fio
+ O Key Mapper usa depuração sem fio para iniciar seu serviço de remapeamento e entrada.
+ Emparelhar depuração sem fio
+ O Key Mapper precisa ser pareado com a depuração sem fio antes de poder iniciar seu serviço de remapeamento e entrada.
+ Iniciar serviço
+ O Key Mapper precisa se conectar ao Android Debug Bridge para iniciar o serviço do modo PRO.
+ Permitir notificações
+ O Key Mapper precisa de permissão para notificá-lo caso haja algum problema com o processo de configuração.
+ Dê permissão
+ Assistente de configuração
+ O modo PRO está em execução
+ Agora você pode remapear botões quando a tela estiver desligada e usar mais ações.
+ Terminar
+ Habilitar opções do desenvolvedor
+ Toque repetidamente no número da compilação
+ Emparelhamento automático
+ Procurando código de pareamento e porta…
+ Não é possível encontrar a porta e o código de emparelhamento
+ Toque no botão para parear com o código de pareamento e digite o código aqui
+ Falha ao iniciar o modo PRO
+ Toque para configurar novamente. Tente o pareamento ADB e reinicie o telefone se o problema persistir.
+ Modo PRO de inicialização automática
+ Usando root
+ Usando shizuku
+ Usando ADB via WiFi
+ Modo PRO iniciado
+ Divirta-se remapeando! ❤️
+ Falha no emparelhamento
+ Mantenha o pop-up do código de emparelhamento na tela ao enviar o código de emparelhamento
+ Código de pareamento de entrada
+ O que posso fazer com o modo PRO?
+ 📲 Você pode remapear mais botões, como o botão de energia.
⌨️ Use qualquer teclado com ações de código de tecla.
⭐️ As seguintes ações estão desbloqueadas: Wi-Fi, Bluetooth, dados móveis, NFC, modo avião, recolher barra de status e ligar/desligar a tela do dispositivo.
- Mostrar informações do modo PRO
- Descartar
+ Mostrar informações do modo PRO
+ DescartarO modo PRO parou inesperadamenteReiniciando automaticamente…Não reinicia automaticamente. Se você não estiver encerrando o serviço, informe o problema ao desenvolvedor.
@@ -1413,10 +1413,10 @@ Deixe em branco se alguma rede Wi-Fi precisar ser correspondida.HabilitarEm execuçãoComprado
- Modo PRO
- Ativar gratuitamente
- Em execução
- Não disponível nesta versão do Android
+ Modo PRO
+ Ativar gratuitamente
+ Em execução
+ Não disponível nesta versão do AndroidSe o assistente do seu dispositivo for acionado por um botão de hardware, talvez seja possível remapeá-lo gratuitamente com o modo PRO. Tente escolher \"Outro\" na página de acionamento e siga as instruções.Este gatilho requer alguma configuração que varia de acordo com o dispositivo. Por favor, leia as instruções
diff --git a/base/src/main/res/values-tr/strings.xml b/base/src/main/res/values-tr/strings.xml
index 8175a0e00b..71fb475b1f 100644
--- a/base/src/main/res/values-tr/strings.xml
+++ b/base/src/main/res/values-tr/strings.xml
@@ -56,11 +56,15 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.Tuş eşleme kimliğiRahatsız Etmeyin modunda düzgün çalışması için izin gerekli!Bu tetikleyici, telefon çalarken veya görüşme sırasında çalışmaz!
- Android, telefonunuz çalarken veya bir görüşme sırasında erişilebilirlik servislerinin ses düğmesi basışlarını algılamasına izin vermez, ancak giriş yöntemi servisleri bunu algılayabilir. Bu nedenle, bu tetikleyicinin çalışmasını istiyorsanız Key Mapper klavyelerinden birini kullanmalısınız.
+ Android, telefonunuz çalarken veya bir telefon görüşmesi sırasında erişilebilirlik servislerinin ses düğmesine basılmasını algılamasına izin vermez, ancak giriş yöntemi servislerinin bunları algılamasına izin verir. Bu nedenle, bu tetikleyicinin çalışmasını istiyorsanız Key Mapper Giriş Yöntemi\'ni kullanmalısınız.Android sınırlamaları nedeniyle harekette çok fazla parmak var.Android sınırlamaları nedeniyle hareket süresi çok yüksek.
- DPAD tetikleyicilerinin çalışması için bir Key Mapper klavyesi kullanmalısınız!
- Tuş eşlemeleriniz rastgele duracak!
+ DPAD tetikleyicilerinin çalışması için Key Mapper Giriş Yöntemi\'ni kullanıyor olmalısınız!
+ PRO modu bu Android sürümünde desteklenmiyor
+ PRO modu başlatılmadı!
+ Tetikleyici cihaz bağlı değil!
+ Bu ekran kapalı tetikleyicisini taşıyın
+ Pil optimizasyonunu devre dışı bırakın.Tuş eşlemeleriniz duraklatıldı!Devam ettirTuş eşlemelerinizin çalışması için erişilebilirlik servisinin açık olması gerekir!
@@ -78,10 +82,10 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
%s uygulamasını açEkrana dokun (%d, %d)Ekrana dokun (%s)
- %d parmakla %d/%d koordinatlarından %d/%d koordinatlarına %dms içinde kaydır
- %d parmakla %d/%d koordinatlarından %d/%d koordinatlarına %dms içinde kaydır (%s)
- %s, %d parmakla %d/%d koordinatlarında %dpx sıkıştırma mesafesiyle %dms içinde
- %s, %d parmakla %d/%d koordinatlarında %dpx sıkıştırma mesafesiyle %dms içinde (%s)
+ %d parmakla (%d,%d) konumundan (%d,%d) konumuna %dms içinde kaydır
+ %d parmakla (%d,%d) konumundan (%d,%d) konumuna %dms içinde kaydır (%s)
+ %d parmakla (%d,%d) üzerinde %dpx sıkıştırma mesafesiyle %dms içinde %s
+ %d parmakla (%d,%d) üzerinde %dpx sıkıştırma mesafesiyle %dms içinde %s (%s)%s numarayı araSes çal: %sBilinmeyen sesi oynat
@@ -113,6 +117,8 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Girilecek metinAçılacak URLAranacak telefon numarası
+ SMS gönderilecek telefon numarası
+ Gönderilecek mesajEylemKategorilerVeri
@@ -168,6 +174,8 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Kaydedilmiş ses dosyanız yok!Key Mapper, Shizuku kullanarak kendine WRITE_SECURE_SETTINGS izni verdiKey Mapper, Root kullanarak kendine WRITE_SECURE_SETTINGS izni verdi
+ Mikrofon sessize alındı
+ Mikrofonun sesi açıldıSıra tetikleyici zaman aşımı
@@ -216,6 +224,12 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Yatay (270°)Dikey (herhangi)Yatay (herhangi)
+ Ekran yönü
+ Fiziksel yön
+ Fiziksel: Dikey
+ Fiziksel: Yatay
+ Fiziksel: Dikey (ters)
+ Fiziksel: Yatay (ters)Medya oynatan uygulamaMedya oynatmayan uygulamaMedya oynuyor
@@ -226,10 +240,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
WiFi kapalıBir WiFi ağına bağlıBir WiFi ağından bağlantı kesildi
- Android 10 ve daha yeni sürümlerde uygulamaların bilinen WiFi ağlarının listesini sorgulamasına izin verilmez, bu nedenle SSID’yi manuel olarak yazmanız gerekecek.
-
- Herhangi bir WiFi ağının eşleşmesi gerekiyorsa boş bırakın.
- Herhangi biri
+ Herhangi bir WiFi ağıyla eşleşmesi gerekiyorsa boş bırakın.%s WiFi’ye bağlı%s WiFi’den bağlantı kesildiHerhangi bir WiFi’ye bağlı
@@ -238,6 +249,10 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
%s seçildiGiriş yöntemi seçilmedi%s seçilmedi
+ Klavye gösteriliyor
+ Ekran klavyesi görünür
+ Klavye gösterilmiyor
+ Ekran klavyesi gizliCihaz kilitliCihaz kilidi açıkKilit ekranı gösteriliyor
@@ -247,6 +262,10 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Telefon çalıyorŞarj oluyorŞarj olmuyor
+ Menteşe kapalı
+ Menteşe açık
+ Menteşe kapalı
+ Menteşe açıkDikey (0°)Yatay (90°)Dikey (180°)
@@ -295,7 +314,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Her şeyi yedekleDokunarak duraklatDokunarak devam ettir
- Kaydet
+ PaylaşKısa mesajları aç/kapatKopyalaTemizle
@@ -303,7 +322,6 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Eylem ekleTetikleyiciyi kaydetmek için dokunun
- Gelişmiş tetikleyicilerYENİ!BittiDüzelt
@@ -331,47 +349,23 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Kaydedilmemiş değişiklikleriniz var. Bunları iptal ederseniz, düzenlemeleriniz kaybolacak.Lütfen Magisk gibi root yönetim uygulamanızda Key Mapper\'a root izni verin.WRITE_SECURE_SETTINGS izni ver
- Bu izni vermek için bir PC/Mac gereklidir. Çevrimiçi kılavuzu okuyun.
- Cihazınızda erişilebilirlik servisleri ayarları sayfası yok gibi görünüyor. Bunu nasıl düzelteceğinizi açıklayan çevrimiçi kılavuzu okumak için “kılavuz” seçeneğine dokunun.
- Tuşlar, basılı tutulacakları sırayla yukarıdan aşağıya listelenmelidir.
- Bir “sıra” tetikleyicisi, paralel tetikleyicilerden farklı olarak bir zaman aşımına sahiptir. Bu, ilk tuşa bastıktan sonra tetikleyicideki diğer tuşları girmeniz için belirli bir süreniz olacağı anlamına gelir. Tetikleyiciye eklediğiniz tüm tuşlar, zaman aşımı süresine ulaşılana kadar normal işlevlerini yerine getirmez. Bu zaman aşımını “Seçenekler” sekmesinden değiştirebilirsiniz.
+ Bu izni vermek için PRO modunu kullanmanız gerekecektir.
+ Cihazınızda bir erişilebilirlik servisleri ayarları sayfası görünmüyor. PRO modunu kurabilir veya \"adb shell pm grant io.github.sds100.keymapper android.permission.WRITE_SECURE_SETTINGS\" ADB komutunu çalıştırabilirsiniz.
+ Tuşları listelendikleri sırayla basılı tutmalısınız.
+ Bu tetikleyiciyi girmek için bir zaman aşımı vardır. Bu zaman aşımını \"Seçenekler\" sekmesinden değiştirebilirsiniz.
+ Bu tetikleyici nasıl kullanılır
+ Sıralı tetikleyicilerAndroid, uygulamaların bağlı (eşleştirilmemiş) Bluetooth cihazlarının listesini almasına izin vermez. Uygulamalar yalnızca bu cihazların bağlandığını ve bağlantısının kesildiğini algılayabilir. Bu nedenle, Bluetooth cihazınız erişilebilirlik servisi başladığında zaten bağlıysa, uygulamanın bunu bilmesi için cihazı yeniden bağlamanız gerekecek.Otomatik yedeklemeKonumu değiştir veya otomatik yedeklemeyi kapat?
- PIN veya Desen gibi başka bir ekran kilidiniz varsa endişelenmenize gerek yok. Ancak yalnızca Parola ekran kilidi kullanıyorsanız, Key Mapper Temel Giriş Yöntemi’ni kullanırsanız telefonunuzun kilidini açamazsınız çünkü bu yöntemin bir arayüzü yoktur. Key Mapper’a WRITE_SECURE_SETTINGS izni vererek klavyeyi değiştirmek için bir bildirim gösterebilirsiniz. Bunu nasıl yapacağınızı öğrenmek için ekranın altındaki soru işaretine dokunun.
+ PIN veya Desen gibi başka bir ekran kilidi seçtiyseniz endişelenmenize gerek yok. Ancak Şifreli bir ekran kilidiniz varsa ve Key Mapper Giriş Yöntemi\'ni kullanıyorsanız, grafik arayüzü olmadığı için telefonunuzun kilidini açamazsınız. Key Mapper\'a WRITE_SECURE_SETTINGS izni vererek klavyeye geçiş yapmak için bir bildirim göstermesini sağlayabilirsiniz. Ekranın altındaki soru işaretine dokunarak bunu nasıl yapacağınıza dair bir kılavuz bulabilirsiniz.Eylemler için bir giriş yöntemi gerektiren bir yöntem seçin. Bunu daha sonra ana ekranın alt menüsündeki “Eylemler için klavye seç” seçeneğiyle değiştirebilirsiniz.
- Caps Lock tuşunun hala büyük harf kilitlemesini engellemek için klavyenizde “Caps Lock’u kameraya” klavye düzenini seçmeniz gerekir. Bu ayarı cihaz ayarlarınızda -> Diller ve Giriş -> Fiziksel Klavye -> Klavyenize dokunun -> Klavye Düzenlerini Ayarla bölümünden bulabilirsiniz. Bu, Caps Lock tuşunu KEYCODE_CAMERA’ya yeniden eşleyerek Key Mapper’ın bunu doğru şekilde eşlemesini sağlar.\n\nBunu yaptıktan sonra Caps Lock tetikleyici tuşunu kaldırıp Caps Lock tuşunu tekrar kaydetmelisiniz. Adımları doğru yaptıysanız “Caps Lock” yerine “Kamera” yazmalıdır.Bağlı harici cihaz yok.
- Key Mapper GUI Klavyesini Yükle
- Bu şiddetle tavsiye edilir! Bu, Key Mapper ile kullanabileceğiniz uygun bir klavyedir. Key Mapper’a dahili olan (Temel Giriş Yöntemi) klavyede ekran klavyesi yoktur. Nereden yüklemek istediğinizi seçin.
- Key Mapper Leanback Klavyesini Yükle
- Bu şiddetle tavsiye edilir! Bu, Key Mapper ile kullanabileceğiniz Android TV için uygun bir klavyedir. Key Mapper’a dahili olan (Temel Giriş Yöntemi) klavyede ekran klavyesi yoktur. Nereden yüklemek istediğinizi seçin.
- Key Mapper GUI Klavyesini Yükle
- Nereden indirmek istediğinizi seçin.
- Key Mapper Leanback Klavyesini Yükle
- Nereden indirmek istediğinizi seçin.
- Bu eylem için ek kurulum gerekiyor
- Bu eylemi kullanmak için cihazınızı ayarlamanın 3 yolu var. Her birinin avantajları ve dezavantajları şunlardır:
-
- \n\n1. Shizuku’yu indirin (önerilen). Şu anda kullandığınız ekran klavyesini değiştirmeniz gerekmez, ancak cihazınızı her yeniden başlattığınızda bir dakikalık kurulum gerektirir.
-
- \n\n2. Key Mapper GUI Klavyesini indirin. Bu, Key Mapper ile kullanabileceğiniz bir ekran klavyesidir, ancak şu anda kullandığınız klavyeyi (örneğin Gboard) kullanamazsınız.
-
- \n\n3. Hiçbir şey yapmayın ve dahili Key Mapper klavyesini kullanın. Bu önerilmez çünkü Key Mapper’ı kullandığınızda hiçbir ekran klavyeniz olmaz! Hiçbir avantajı yoktur.
- Bu eylem için ek kurulum gerekiyor
- Bu eylemi kullanmak için cihazınızı ayarlamanın 3 yolu var. Her birinin avantajları ve dezavantajları şunlardır:
-
- \n\n1. Shizuku’yu indirin (önerilen). Şu anda kullandığınız ekran klavyesini değiştirmeniz gerekmez, ancak cihazınızı her yeniden başlattığınızda bir dakikalık kurulum gerektirir.
-
- \n\n2. Key Mapper Leanback Klavyesini indirin. Bu, Key Mapper ile kullanabileceğiniz Android TV için optimize edilmiş bir ekran klavyesidir, ancak şu anda kullandığınız klavyeyi (örneğin Gboard) kullanamazsınız.
-
- \n\n3. Hiçbir şey yapmayın ve dahili Key Mapper klavyesini kullanın. Bu önerilmez çünkü Key Mapper’ı kullandığınızda hiçbir ekran klavyeniz olmaz! Hiçbir avantajı yoktur.Pil optimizasyonunu devre dışı bırakHEPSİNİ okumanız ZORUNLU, aksi takdirde ileride sinir bozucu sorunlar yaşarsınız!\n\n“Kısmen düzelt” seçeneğine dokunmak, Android’in uygulamayı arka planda durdurmasını belki engelleyebilir.\n\nBu YETERLİ DEĞİL. MIUI veya Samsung Experience gibi OEM arayüzünüzde başka uygulama kapatma özellikleri olabilir, bu nedenle dontkillmyapp.com’daki çevrimiçi kılavuzu takip ederek Key Mapper için bunları da kapatmalısınız.Erişilebilirlik servisini kapatıpaçarak yeniden başlatın.
- Bu tetikleyiciyi kullanmak, cihazınızın ayarlarındaki ekran sabitleme ayarını kullandıktan sonra cihazınızın kilidini açtığınızda siyah bir ekran oluşmasına neden olabilir. Bu, bir yeniden başlatma ile düzeltilebilir. Bu durum tüm cihazlarda olmaz, bu yüzden dikkatli olun ve sorun yaşarsanız ayarı kapatın!Key Mapper kesintiye uğradı
- Key Mapper arka planda çalışmaya çalıştı ancak sistem tarafından durduruldu.\nBu, pil veya bellek optimizasyonu açık olduğunda olabilir.\n\nBunu düzeltmek için çevrimiçi bir kılavuzu takip edebilirsiniz. İşiniz bittiğinde servisi de yeniden başlatmalısınız.
+ Key Mapper arka planda çalışmayı denedi ancak sistem tarafından durduruldu.\nBu durum, pil veya bellek optimizasyonu açık olduğunda meydana gelebilir.\n\nBunu düzeltmek için çevrimiçi bir kılavuzu takip etmeyi deneyebilirsiniz. İşiniz bittiğinde servisi de yeniden başlatmalısınız.Devam etYoksayHata raporu oluşturulamadı
@@ -380,14 +374,10 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Key Mapper için dosya seçmenize izin veren bir dosya uygulamanız yüklü değil. Lütfen bir dosya yöneticisi yükleyin.Erişilebilirlik servisi etkinleştirilmeli@string/accessibility_service_explanation
+ Kısıtlanmış Ayarlara izin vermeniz gerekebilir.
+ Talimatları okumak için dokunun.Rahatsız Etmeyin erişimini verCihazınızın hangi uygulamaların Rahatsız Etmeyin durumunu değiştirebileceğini yönetebileceğiniz ayarlar sayfasına yönlendirileceksiniz. Bu bazı cihazlarda mevcut değildir, bu yüzden listede Key Mapper’ı görmüyorsanız “tekrar gösterme” seçeneğine dokunun.
- Bilmenizde fayda var!
- Bir tetikleyici tuşunun yanında bu sembolü (⌨) görüyorsanız, algılanması için bir Key Mapper klavyesi KULLANMALISINIZ. Bu, Android’deki bir kısıtlamadır ve yalnızca bazı düğmeler için gereklidir.
- Önemli!
- Key Mapper GUI Klavyesini, bu Key Mapper sürümüyle uyumlu olacak şekilde güncellemelisiniz. Güncelleme yapana kadar bazı tuş eşlemeleri çalışmayabilir!
- Şimdi güncelle
- YoksayŞuna göre sıralaÖncelikleri ayarlamak için tutamaçları sürükleyin. En üstteki öğe en önemlisidir. Ayrıca herhangi bir öğeye dokunarak sıralama sırasını tersine çevirebilirsiniz.Örnek: Tuş eşlemelerini öncelikle Eylemlerine göre artan sırayla ve ikincil olarak Tetikleyicilerine göre azalan sırayla sıralamak için Eylemleri birinci sıraya, Tetikleyicileri ikinci sıraya taşıyın.
@@ -395,9 +385,14 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Örnek gösterBildirimleri açBazı eylemler ve seçenekler bu izni gerektirir. Ayrıca uygulama hakkında önemli haberler olduğunda bildirim alabilirsiniz.
+ Bu ekran kapalı tetikleyicisini taşıyın
+ Kesinti için üzgünüz. Eski ekran kapalı yeniden eşleme seçeneğinin yerini alan PRO modu adında *ücretsiz* bir özellik getirdik. Bu özellik çok daha güvenilir ve güç düğmesini yeniden eşleme özelliğini de açıyor.\n\nÇalışma şekli nedeniyle bu tetikleyiciyi PRO modu ile yeniden kaydetmeniz gerekecek.
+ Devam et
+ READ_LOGS iznini verin
+ Bunun çalışması için rootlu olmanız veya PRO modunu etkinleştirmiş olmanız gerekir. Sistem bu izni verdiğinde uygulamayı kapatacaktır. Uygulamayı tekrar kendiniz açmanız ve logcat\'i tekrar paylaşmayı denemeniz gerekecek.Bitti
- Kılavuz
- Kılavuz
+ Durdur
+ PRO modunu kullanDeğiştirKısmen düzeltTamam
@@ -408,6 +403,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
KaydetAnladımAç
+ Devam etKapatİptalTekrar gösterme
@@ -418,17 +414,14 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
AyarlarBelgelerDeğişiklik günlüğü
- Shizuku
- Key Mapper GUI Klavyesi
- Key Mapper Leanback Klavyesi
- Hiçbir şey yapmaDüzeltTuş eşlemelerini duraklat/devam ettirKlavye gizli uyarısı
- Key Mapper klavyesini aç/kapat
+ Key Mapper Giriş Yöntemini DeğiştirYeni özellikler
+ Özel bildirimlerÇalışıyorKey Mapper’ı açmak için dokunun.Duraklat
@@ -445,9 +438,11 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Servisi durdurKlavye gizli!Klavyeyi tekrar göstermeye başlamak için ‘klavyeyi göster’e dokunun.
- Key Mapper klavyesini aç/kapat
- Key Mapper klavyesine ve klavyenizden geçiş yapmak için ‘aç/kapat’a dokunun.
+ Key Mapper Giriş Yöntemini Değiştir
+ Key Mapper Giriş Yöntemi\'ne geçiş yapmak için \'değiştir\'e dokunun.Aç/Kapat
+ Ekran kapalı tuş eşlemeleriniz dikkat gerektiriyor!
+ Kesinti için üzgünüz ancak ekran kapalı tuş eşlemelerinizi yeniden kaydetmeniz gerekecek.Varsayılan uzun basış gecikmesi
@@ -471,19 +466,19 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Otomatik yedeklemeyi açTuş eşlemelerinizi periyodik olarak yedekleyinBir cihaz (örneğin klavye) bağlandığında/bağlantısı kesildiğinde ekran klavyesini otomatik olarak değiştir
- Seçilen bir cihaz bağlandığında son kullanılan Key Mapper klavyesi otomatik olarak seçilir. Cihazın bağlantısı kesildiğinde normal klavyeniz otomatik olarak seçilir.
+ Seçilen bir cihaz bağlandığında Key Mapper Giriş Yöntemi otomatik olarak seçilecektir. Cihazın bağlantısı kesildiğinde ise normal klavyeniz otomatik olarak seçilecektir.Metin girmeye başladığınızda ekran klavyesini otomatik olarak değiştir
- Klavyeyi açmaya çalıştığınızda son kullanılan Key Mapper dışı klavye otomatik olarak seçilir. Klavyeyi kullanmayı bıraktığınızda Key Mapper klavyeniz otomatik olarak seçilir.
+ Klavyeyi açmaya çalıştığınızda son kullanılan normal ekran klavyesi otomatik olarak seçilecektir. Klavyeyi kullanmayı bıraktığınızda ise Key Mapper Giriş Yöntemi otomatik olarak seçilecektir.Ekran üstü mesajKlavyeyi otomatik olarak değiştirirken gösterRoot izni isteCihazınız rootlu ise bu, Magisk\'ten veya root uygulamanızdan root izni penceresini gösterecektir.Tema seçAçık ve koyu temalar mevcut
- Bildirime dokunduğunuzda Key Mapper klavyesi ile varsayılan klavyeniz arasında geçiş yapın.
- Key Mapper klavyesini aç/kapat bildirimi
+ Bildirime dokunduğunuzda Key Mapper Giriş Yöntemi ile normal klavyeniz arasında geçiş yapın.
+ Key Mapper Giriş Yöntemini değiştir bildirimini gösterTuş eşlemelerini açarken/kapatırken klavyeyi otomatik olarak değiştir
- Tuş eşlemelerinizi devam ettirdiğinizde Key Mapper klavyesini, duraklattığınızda ise varsayılan klavyenizi otomatik olarak seçin.
+ Tuş eşlemelerinizi devam ettirdiğinizde Key Mapper Giriş Yöntemi\'ni otomatik olarak seçin ve duraklattığınızda varsayılan klavyenizi seçin.Ana ekran uyarılarını gizleAna ekranın üstündeki uyarıları gizleCihaz kimliklerini göster
@@ -525,17 +520,14 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Bu seçenekler yalnızca rootlu cihazlarda çalışır! Rootun ne olduğunu veya cihazınızın rootlu olup olmadığını bilmiyorsanız, bunlar çalışmazsa lütfen kötü bir inceleme bırakmayın. :)WRITE_SECURE_SETTINGS izni gerektirirBu seçenekler yalnızca Key Mapper WRITE_SECURE_SETTINGS iznine sahipse etkinleşir. İzni nasıl vereceğinizi öğrenmek için aşağıdaki düğmeye tıklayın.
- Shizuku desteği
- Shizuku, Key Mapper’ın yalnızca sistem uygulamalarının yapabileceği şeyleri yapmasını sağlayan bir uygulamadır. Örneğin, Key Mapper klavyesini kullanmanız gerekmez. Bunu nasıl kuracağınızı öğrenmek için dokunun.
- Shizuku\'yu kurmak için bu adımları izleyinKlavyeyi otomatik olarak değiştirGerektiğinde değiştir, sonra geri dönCihazları seçHangi cihazların otomatik klavye değiştirmeyi tetikleyeceğini seçinGünlük kaydıBu, tuş eşlemelerinize gecikme ekleyebilir, bu yüzden yalnızca uygulamayı hata ayıklamaya çalışıyorsanız veya geliştirici tarafından istenmişse açın.
- PRO modunu kullan
- Gelişmiş tuş olayı algılama ve daha fazlası
+ PRO modunu kullan
+ Gelişmiş tuş olayı algılama ve daha fazlasıAçıkKoyuSistem
@@ -555,7 +547,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Basılı tutTekrar basılana kadar basılı tutYeniden eşleme yapma
- Bu tuş eşlemesini diğer uygulamaların tetiklemesine izin ver
+ Diğer uygulamaların bu tuş eşlemesini intent\'ler veya kısayollarla kontrol etmesine izin verTuş eşleme kimliğini kopyala⌨
@@ -584,7 +576,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
ÇalışıyorServis Devre DışıKey Mapper erişilebilirlik servisi devre dışı
- Key Mapper klavyesini aç/kapat
+ Key Mapper Giriş Yöntemini DeğiştirCtrl
@@ -606,7 +598,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Scroll Lock
- Bu eylemin çalışması için Key Mapper klavyelerinden birini kullanıyor olmanız gerekiyor!
+ Bu eylemin çalışması için Key Mapper Giriş Yöntemi\'ni kullanıyor olmalısınız!%s paket adına sahip uygulama yüklü değil!%s uygulaması devre dışı!Key Mapper\'a sistem ayarlarını değiştirme izni vermeniz gerekiyor.
@@ -621,7 +613,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Cihazınız Bluetooth\'u desteklemiyor.Cihazınız cihaz politikası uygulamasını desteklemiyor.Cihazınızda kamera flaşı bulunmuyor.
- Cihazınızda telefon özellikleri bulunmuyor.
+ Cihazınız telefon özelliklerini kullanamıyor. SIM kart takılı mı?Klavye ayarları sayfası bulunamadı!Key Mapper\'ın cihaz yöneticisi olması gerekiyor!Key Mapper\'ın bu kısayolu kullanma izni yok
@@ -632,6 +624,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Rahatsız Etmeyin erişim izni ayarları bulunamadı!Key Mapper\'ın WRITE_SECURE_SETTINGS iznine ihtiyacı var.Bu telefon aramasını başlatabilecek bir uygulama yok
+ Bu SMS\'i gönderebilecek bir uygulama yokKamera kullanımda!Kamera bağlantısı kesildi!Kamera devre dışı!
@@ -643,11 +636,12 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Erişilebilirlik servisinin etkinleştirilmesi gerekiyor!Erişilebilirlik servisinin yeniden başlatılması gerekiyor!Başlatıcınız kısayolları desteklemiyor.
- Bir Key Mapper klavyesinin etkinleştirilmesi gerekiyor!
+ Key Mapper Giriş Yöntemi\'nin etkinleştirilmesi gerekiyor!%s giriş yöntemi bulunamadıGiriş yöntemi seçici gösterilemiyor!Erişilebilirlik düğümü bulunamadı!%s genel eylemi gerçekleştirilemedi!
+ Bu eylemin ayarlanması gerekiyorPil optimizasyon ayarları bulunamadı! Varsa, manuel olarak açın.Ekstra (%s) bulunamadı!Aynı kısıtlamaya iki kez sahip olamazsınız!
@@ -664,10 +658,21 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Bildirim erişim izni reddedildi!Geçersiz!Telefon araması başlatma izni reddedildi!
+ SMS mesajı gönderme izni reddedildi!
+ SMS gönderilemedi. Numara doğru mu?
+ SMS gönderilemiyor. Uçak modunu kapatın.
+ SMS gönderilemiyor. Hücresel servis yok.
+ SMS gönderme sınırı aşıldı. Daha sonra tekrar deneyin.
+ Ağ SMS\'i reddetti.
+ SMS gönderilemiyor. Cihazın belleği dolu.
+ Geçersiz SMS mesaj formatı.
+ SMS gönderilemiyor. Ağ hatası.
+ Acil arama sırasında SMS gönderilemez.
+ SMS gönderilemiyor. SIM kart algılanmadı.Bu yedeği kullanmak için Key Mapper\'ı en son sürüme güncellemeniz gerekiyor.Sesli asistan yüklü değil!Yetersiz izinler
- Yalnızca Key Mapper klavyeleri yüklü!
+ Yüklü ekran klavyeniz yok!Medya oynatan bir uygulama yok!Kaynak dosya bulunamadı! %sHedef dosya bulunamadı! %s
@@ -675,6 +680,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Sistem ayarı %s değiştirilemedi!%s etkinleştirilmeli!Giriş yöntemi değiştirilemedi!
+ Giriş yöntemi etkinleştirilemedi!Cihazınızda kamera uygulaması yok!Cihazınızda asistan yok!Cihazınızda ayarlar uygulaması yok!
@@ -696,6 +702,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Telefon aramalarını yanıtlama ve sonlandırma izni reddedildi!Eşleştirilmiş Bluetooth cihazlarını görme izni reddedildi!Bildirim gösterme izni reddedildi!
+ Günlükleri okuma izni reddedildi!2 veya daha fazla olmalı!%d veya daha az olmalı!0\'dan büyük olmalı!
@@ -704,7 +711,9 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
0\'dan büyük olmalı!%d veya daha az olmalı!UI öğesi bulunamadı!
+ Komut %1$d saniye sonra zaman aşımına uğradıPRO Modunun başlatılması gerekiyor
+ Hız sınırına ulaşıldı. Saniyede yalnızca bir kez gönderebilirsiniz.WiFi\'yi aç/kapat
@@ -718,6 +727,9 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Sesi kapatSesi aç/kapatSesi aç
+ Mikrofonu sessize al
+ Mikrofonun sesini aç
+ Mikrofon sesini aç/kapatSes diyaloğunu gösterAkışı artır%s akışını artır
@@ -743,6 +755,9 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Mobil veriyi aç/kapatMobil veriyi açMobil veriyi kapat
+ Hotspot\'u aç/kapat
+ Hotspot\'u aç
+ Hotspot\'u kapatOtomatik parlaklığı aç/kapatOtomatik parlaklığı kapatOtomatik parlaklığı aç
@@ -866,9 +881,32 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Telefon araması başlatTelefon aramasını cevaplaTelefon aramasını sonlandır
+ SMS gönder
+ SMS gönder: \"%s\"\" - %s
+ SMS oluştur
+ SMS oluştur: \"%s\" - %s
+ Ayarı ayarla: %1$s = %2$s
+ Sistem
+ Güvenli
+ Genel
+ Ayarı değiştirSes çalEn son bildirimi kaldırTüm bildirimleri kaldır
+ Bildirim oluştur
+ Bildirimi göster: %1$s
+ Bildirim başlığı
+ Bildirim başlığını girin
+ Başlık boş olamaz
+ Bildirim içeriği
+ Bildirim içeriğini girin
+ İçerik boş olamaz
+ Bildirimi otomatik kapat
+ Şu süre sonra otomatik kapat
+ %d saniye
+ Test et
+ Test ediliyor…
+ Bildirim başarıyla gösterildiCihaz kontrol ekranıHTTP isteğiHTTP Yöntemi
@@ -880,6 +918,31 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
İstek gövdesi (opsiyonel)Yetkilendirme başlığı (opsiyonel)Gerekirse \'Bearer\' ön ekini kullanın
+ Shell komutu
+ Shell komutu eylemi
+ Betik
+ Betik boş olamaz!
+ \'adb shell\' yazmayın!
+ Root olarak çalıştır
+ Standart
+ Root
+ ADB
+ Çalıştırma Modu
+ ADB modu akış çıktısını desteklemiyor
+ PRO Modunu Kur
+ PRO Modunu Kur (Desteklenmiyor)
+ Yapılandırma
+ Çıktı
+ Henüz çıktı yok. Komutu çalıştırmak için Test\'e tıklayın.
+ Test et
+ Çıktı
+ Çalıştırılıyor…
+ Başarılı
+ Başarısız
+ Çıkış kodu: %1$d
+ Root ile çalıştır: %s
+ ADB ile çalıştır: %s
+ Çalıştır: %sUygulama öğesiyle etkileşime geçKey Mapper, menüler, sekmeler, düğmeler ve onay kutuları gibi uygulama öğelerini algılayabilir ve bunlarla etkileşime girebilir. Key Mapper\'ın ne yapmak istediğinizi bilmesi için, uygulama öğesiyle etkileşiminizi kaydetmeniz gerekmektedir.Kaydetmeye başla
@@ -916,6 +979,20 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Kaynak kimliğini görüntüleÖzgün kimlikEtkileşim türü
+ Uygulamayı durmaya zorla
+ Uygulamayı son uygulamalardan kapat ve temizle
+ Ayarı değiştir
+ Anahtar
+ Değer
+ Ayar anahtarı boş olamaz
+ Ayar değeri boş olamaz
+ Not: Yalnızca ayar değerlerini değiştirmek, sistemin değişikliği işlemesi için yeterli olmayabilir. Bazı ayarların etkili olması için ek eylemler veya yayınlar gerekir.
+ Test et
+ Ayar başarıyla değiştirildi
+ İzin ver
+ Ayar seçin
+ Ayar bulunamadı
+ Mevcut ayarı seçinNavigasyon
@@ -932,6 +1009,20 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
BildirimlerÖzel
+
+ Uygulamalar
+ Medya
+ Bluetooth
+ Ekran
+ El feneri
+ WiFi
+ Klavye
+ Kilit
+ Telefon
+ Güç
+ Cihaz
+ Zaman
+
BooleanBoolean dizisi
@@ -972,15 +1063,6 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Java programlama dilinde geçerli Kısa sayıların virgülle ayrılmış bir listesi. Örn. 3242,12354Intent bayrakları bit bayrakları olarak saklanır. Bu bayraklar Intent\'in nasıl işleneceğini değiştirir. Bir Etkinlik Intent\'i için bu alan boş bırakılırsa, Key Mapper varsayılan olarak FLAG_ACTIVITY_NEW_TASK kullanır. Daha fazla bilgi için Android geliştirici belgelerini görmek üzere \'dokümanlar\'a dokunun.
-
- Öğreticiyi geç
- İlk anahtar haritanı oluştur!
- Bir anahtar haritası, bir düğmeye basıldığında cihazınızın ne yapacağını belirten bir kuraldır.
- Bir eylem seçin
- Bir eylem, tetikleyiciye bastığınızda olması gereken şeydir.
- Bir kısıtlama seç (isteğe bağlı)
- Anahtar haritasının yalnızca belirli durumlarda çalışmasını istiyorsanız, örneğin, bir uygulama açıkken.
-
GitHubWeb Sitesi
@@ -1001,24 +1083,42 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Çevirmen (Çekçe)Çevirmen (İspanyolca)
+
+ Key Mapper\'ı Destekleyin ❤️
+ Deneyiminizi yükseltmek için eklentileri seçin
+ Desteğiniz Key Mapper\'ı ayakta tutuyor!
+ "Kayan düğme özelliği ezber bozan bir yenilik"
+ Google Play yorumcusu
+ Kayan düğmeler ezber bozan bir yenilik!
+ Kayan düğmeler özelliğini keyifle kullanıyorum.
+ Kayan düğmeler harika bir eklenti.
+ Telefon deneyimimin vazgeçilmez bir parçası.
+ Bir kez öde, sonsuza dek kullan
+ Videoyu izle
+ Şimdi satın al (%s)
+ Daha fazla bilgi al
+ Ekran üstü kayan düğmeler
+ Yeterli düğmeniz yok mu? Herhangi bir uygulamada, oyunda veya kilit ekranınızda anında kısayollar ve makrolar oluşturun!
+ Oyunlarda kullan
+ Kilit ekranında kullan
+ Yan tuş ve Asistan tetikleyicisi
+ İşe yaramaz yan tuş mu? Asistan düğmesini veya cihazınızın yan tuş kısayolunu istediğiniz her şeye yeniden atayın!
+ Ekran kapalı
+ Bixby
+ GeminiKey Mapper: Yan TuşHerhangi bir asistanYan tuş/güç düğmesiSesli asistan
- Gelişmiş tetikleyiciler
- Geliştirici, reklamların sürdürülebilir veya kullanıcı dostu bir gelir modeli olduğuna inanmıyor, bu nedenle bu ücretli tetikleyiciler geliştirmeyi desteklemeye yardımcı oluyor ❤️. Ayrıca öncelikli destek de alacaksınız.
- Yan tuş ve Asistan tetikleyicisi
- Yan tuşunuzu, güç düğmenizi veya cihaz asistanınızı yeniden eşleyebileceğinizi biliyor muydunuz? Asistanı veya güç menüsünü başlatmak yerine, cihazınız seçtiğiniz bir eylemi gerçekleştirebilir. Ekran kapalıyken bile çalışır!Yan tuş tetikleyici özelliğini satın almanız gerekiyor.
- Daha fazla bilgiKayan düğmeleri satın almanız gerekiyor.Düğme silindi.Kayan düğmeSatın alma doğrulanamıyor. İnternet bağlantınız var mı?Kilidi aç (%s)
- Kullan
+ KullanYükleniyor…Satın alındı!Fiyatı tekrar almayı dene
@@ -1033,25 +1133,12 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Bir şeyler ters gitti 😕Tekrar deneGeliştiriciyle iletişime geç
- Yan tuş tetikleyici özelliğini satın almanız gerekiyor! Tuş eşlemesine dokunun ve ardından \'Gelişmiş tetikleyiciler\'e tıklayarak satın alın.
- Kayan düğmeler özelliğini satın almanız gerekiyor! Tuş eşlemesine dokunun ve ardından \'Gelişmiş tetikleyiciler\'e tıklayarak satın alın.
- Uygulamayı desteklediğiniz için teşekkürler ❤️!
+ Yan tuş tetikleyici özelliğini satın almalısınız! Tuş eşlemesine dokunun ve ardından mağazaya tıklayarak satın alın.
+ Kayan düğmeler özelliğini satın almalısınız! Tuş eşlemesine dokunun ve ardından mağazaya tıklayarak satın alın.
+ Uygulamayı desteklediğiniz için teşekkürler!Satın alımınız başarılı oldu. Key Mapper\'ın ücretli bir kullanıcısı olarak uygulamayı kullanmanıza yardımcı olmak için öncelikli destek alacaksınız. Artık mağazada geliştiriciyle iletişime geçmek için bir düğme bulunmaktadır.
- Gelişmiş tetikleyiciler ücretli bir özelliktir ancak siz FOSS yapısını indirdiniz ve bu yapı Google Play faturalandırmasını içermiyor. Bu özelliğe erişmek için lütfen Key Mapper\'ı Google Play\'den indirin.
+ Gelişmiş tetikleyiciler ücretli bir özelliktir ancak siz, bu kapalı kaynak modülünü veya Google Play faturalandırmasını içermeyen Key Mapper\'ın FOSS sürümünü indirdiniz. Bu özelliğe erişmek için lütfen Key Mapper\'ı Google Play\'den indirin.Play sürümünü indir
-
- DPAD düğmelerini yeniden eşlemek ister misiniz?
- Aşağıdaki adımları izleyerek Key Mapper GUI Klavyesini ayarlamanız gerekiyor.
- 1. Klavye uygulamasını yükle
- Yükle
- Yüklendi
- 2. Klavyeyi etkinleştir
- Etkinleştir
- Etkin
- 3. Klavyeyi kullan
- Klavyeyi değiştir
- Klavye seçildi
- Kurulum tamamlandı! \'Bitti\'ye dokunun ve DPAD tetikleyiciniz çalışmalı.Düğme algılanmadı mı?Erişilebilirlik servisi yerine tetikleyicinizi kaydetmek için Key Mapper GUI Klavye uygulamasını deneyebilirsiniz.
@@ -1078,12 +1165,27 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
İptalBittiDüğmenin metni olmalı!
+ Bildirim panelinin üzerinde göster
+ Klavyenin üzerinde gösterKayan düğmelerKayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz.Kayan düğme %s (%s)Silinen kayan düğme
- Kısıtlamalar
- Bu düğmenin yalnızca bazı uygulamalarda ekranda olmasını ister misiniz? Bu tuş eşlemesi için “Kısıtlamalar” sekmesinde bir “Ön plandaki uygulama” kısıtlaması ekleyin.
+ Daha iyi Caps Lock uyumluluğu
+ PRO modu, Caps Lock yeniden eşlemesi için daha iyi uyumluluk sağlar. \'PRO modunu kullan\' seçeneğine dokunun ve tekrar kaydedin.
+ PRO modunu kullan
+ Ekran kapalıyken yeniden eşleme?
+ PRO modu ile artık herhangi bir tetikleyici ekran kapalıyken ücretsiz olarak çalışabilir! Tekrar kaydetmeniz gerekecek.
+ PRO modunu kullan
+ Uygulama sabitleme uyarısı
+ Geri düğmesini tetikleyici olarak kullanmak uygulama sabitleme ile çakışabilir ve kilit açıldığında siyah ekrana neden olabilir. Yeniden başlatma sorunu çözecektir.
+ Klavye simgesi
+ ⌨ simgesi, bir Android kısıtlaması nedeniyle bu tetikleyicinin çalışması için Key Mapper giriş yöntemini kullanmanız gerektiği anlamına gelir.
+ Zil modu eylemleri
+ Rahatsız Etmeyin ayarlarıyla çakışmaları önlemek için zil modu eylemleri için PRO modunu kullanmayı düşünün.
+ PRO modunu kullan
+ Belirli uygulamalarla sınırla?
+ Kısıtlamalar sekmesinde kısıtlamalar ekleyin.Bir düzen seçinArkaYardım
@@ -1096,8 +1198,6 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Düğmeyi yapılandırDüzeni düzenleAndroid 11 veya daha yeni bir sürüm gerektiriyor.
- Yeterince düğmeniz yok mu? Artık kendi düğmelerinizi yapabilirsiniz!
- Kayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz.TetikleyiciEylemler
@@ -1108,7 +1208,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Yeni tuş eşlemesiYeni düzenBir tuş eşlemesini yapılandırmak için dokunun.\nDaha fazla seçenek için uzun basın.
- Bir tuş eşlemesi oluşturun!
+ Bir tuş eşlemesi oluşturun!\n\nTuş eşlemesi, bir düğmeye basıldığında cihazınıza ne yapacağını söyleyen bir kuraldır.Yeterince düğmeniz yok mu?Kendi düğmelerinizi yapın!Kayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz.
@@ -1131,7 +1231,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Evet, silİptalKayan düzenleri gizle
- Kayan düğmeleri, bir tetikleyici oluştururken Gelişmiş Tetikleyiciler düğmesinde bulabilirsiniz.
+ Tetikleyici oluştururken Mağaza düğmesinde kayan düğmeleri bulabilirsiniz.Kayan düğmelerMenüSırala
@@ -1213,7 +1313,6 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Parmak izi okuyucuda aşağı kaydırParmak izi okuyucuda sola kaydırParmak izi okuyucuda sağa kaydır
- Gelişmiş tetikleyicilerTuş kodu %dTarama kodu %dKodu tara
@@ -1222,7 +1321,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Tuş kodu %d kullanTarama kodu %d kullanKaydedilmiş tarama kodu yok
- PRO modu ile kaydet
+ PRO modu ile kaydetDaha fazla ekleKaldır
@@ -1250,7 +1349,11 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Android 13 veya daha yeni bir sürüm gerektiriyor.Bu cihaz parlaklık değişikliğine izin vermiyor.Parlaklık değişikliği
+ Ses akışı
+ Varsayılan (sistem kontrollü)
+ SeçeneklerDesteklenmiyor
+ DesteklenmiyorTuş eşlemelerinin yalnızca belirli durumlarda çalışmasını istiyorsanız kısıtlamalar ekleyin.Son kullanılan kısıtlamalar
@@ -1278,86 +1381,91 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
+%d devralınan kısıtlamalar
- PRO modu
- Önemli!
- PRO modu ile tuşları yeniden eşlemek tehlikelidir ve yanlış eşlerseniz çalışmalarını durdurabilir.\n\nBir hata yaparsanız, güç ve ses düğmelerini 30 saniye basılı tutarak cihazınızı zorla yeniden başlatmanız gerekebilir — bunun nasıl yapılacağı konusunda cihazınızın kılavuzuna veya internete başvurun.
- %d…
- Anlıyorum
- Anladım
- Kurulum
- Root algılandı
- Key Mapper\'a root izni vererek kurulum sürecini atlayabilirsiniz. Bu, Key Mapper\'ın açılışta PRO modunu otomatik olarak başlatmasını da sağlayacaktır.
- PRO modunu başlat
- Shizuku algılandı
- Key Mapper\'a Shizuku izni vererek kurulum sürecini atlayabilirsiniz.
- Shizuku\'yu başlat
- İzin iste
- PRO modunu başlat
- Key Mapper ile kur
- Devam et
- Devam et (Android 11+)
- Seçenekler
- Tüm tuş eşlemeleri için PRO modunu etkinleştir
- Key Mapper yeniden eşleme için ADB Shell\'i kullanacak
- Bu ayarlar, uyarıyı kabul edene kadar kullanılamaz.
- PRO modu hizmeti çalışıyor
- Durdur
- Açılışta otomatik olarak başlat
- Cihazınızı her açtığınızda veya yeniden başlattığınızda PRO Modu kendini başlatacaktır
- Acil durum ipucu
- Güç düğmeniz çalışmazsa, PRO Modu\'nu devre dışı bırakmak için güç düğmesini 10 saniye basılı tutun ve bırakın.
- Kurulum sihirbazı
- Adım %d / %d
- Etkileşimli kurulum asistanını kullan
- Ayarlarla otomatik olarak etkileşime gir
- Önce erişilebilirlik servisini etkinleştir
- Eğitimi izle
- Servisi başlat
- Ayarlara git
- Servisi başlat
- Erişilebilirlik servisini etkinleştir
- Key Mapper, PRO modunu kurmanıza yardımcı olmak için bir servis kullanır. Sıradan tuş eşlemeleri için de kullanışlıdır.
- Geliştirici seçeneklerini etkinleştir
- Key Mapper\'ın PRO modunu başlatmak için Android Debug Bridge\'i kullanması gerekir ve bunun için geliştirici seçeneklerini etkinleştirmeniz gerekir.
- Bir WiFi ağına bağlan
- Key Mapper\'ın ADB\'yi etkinleştirmek için bir WiFi ağına ihtiyacı var. İnternet bağlantısına ihtiyacınız yok.\n\nWiFi ağı yok mu? Başka birinin telefonundan bir hotspot kullanın.
- Kablosuz hata ayıklamayı etkinleştir
- Key Mapper, yeniden eşleme ve giriş hizmetini başlatmak için kablosuz hata ayıklamayı kullanır.
- Kablosuz hata ayıklamayı eşleştir
- Key Mapper\'ın yeniden eşleme ve giriş hizmetini başlatabilmesi için kablosuz hata ayıklama ile eşleşmesi gerekir.
- Servisi başlat
- Key Mapper\'ın PRO modu hizmetini başlatmak için Android Debug Bridge\'e bağlanması gerekir.
- Bildirimlere izin ver
- Key Mapper\'ın kurulum sürecinde herhangi bir sorun olması durumunda sizi bilgilendirmek için izne ihtiyacı var.
- İzin ver
- Kurulum asistanı
- PRO modu çalışıyor
- Artık ekran kapalıyken tuşları yeniden eşleyebilir ve daha fazla eylem kullanabilirsiniz.
- Bitir
- Geliştirici seçeneklerini etkinleştir
- Yapı numarasına tekrar tekrar dokun
- Otomatik olarak eşleştiriliyor
- Eşleştirme kodu ve portu aranıyor…
- Eşleştirme portu ve kodu bulunamadı
- Eşleştirme koduyla eşleştirmek için düğmeye dokunun ve kodu buraya yazın
- PRO Modu başlatılamadı
- Tekrar kurmak için dokunun. Tekrar tekrar başarısız olursa ADB eşleştirmeyi ve telefonunuzu yeniden başlatmayı deneyin.
- PRO modu otomatik başlatılıyor
- Root kullanılıyor
- Shizuku kullanılıyor
- WiFi üzerinden ADB kullanılıyor
- PRO modu başlatıldı
- Yeniden eşlemede iyi eğlenceler! ❤️
- Eşleştirme başarısız
- Eşleştirme kodunu gönderirken eşleştirme kodu açılır penceresini ekranda tutun
- Eşleştirme kodunu gir
- PRO modu ile ne yapabilirim?
- 📲 Güç düğmeniz gibi daha fazla tuşu yeniden eşleyebilirsiniz.\n⌨️ Tuş kodu eylemleriyle herhangi bir klavyeyi kullanın.\n⭐️ Aşağıdaki eylemlerin kilidi açılır: WiFi, Bluetooth, mobil veri, NFC ve uçak modu, durum çubuğunu daralt ve cihazı uyut/uyandır.
- PRO modu bilgilerini göster
- Kapat
+ PRO modu
+ Kurulum
+ Önemli!
+ PRO modu ile tuşları yeniden eşlemek tehlikelidir ve yanlış eşlerseniz çalışmalarını durdurabilir.\n\nBir hata yaparsanız, güç ve ses düğmelerini 30 saniye basılı tutarak cihazınızı zorla yeniden başlatmanız gerekebilir — bunun nasıl yapılacağı konusunda cihazınızın kılavuzuna veya internete başvurun.
+ %d…
+ Anlıyorum
+ Anladım
+ Kurulum
+ Root algılandı
+ Key Mapper\'a root izni vererek kurulum sürecini atlayabilirsiniz. Bu, Key Mapper\'ın WiFi bağlantısını beklemeden PRO modunu otomatik olarak başlatmasını da sağlar.
+ PRO modunu başlat
+ Shizuku algılandı
+ Key Mapper\'a Shizuku izni vererek kurulum sürecini atlayabilirsiniz.
+ Shizuku\'yu başlat
+ İzin iste
+ PRO modunu başlat
+ Key Mapper ile kur
+ Devam et
+ Devam et (Android 11+)
+ Seçenekler
+ Tüm tuş eşlemeleri için PRO modunu etkinleştir
+ Key Mapper yeniden eşleme için ADB Shell\'i kullanacak
+ Bu ayarlar, uyarıyı kabul edene kadar kullanılamaz.
+ PRO modu hizmeti çalışıyor
+ Durdur
+ Otomatik başlat ve çalışır durumda tut
+ PRO Modu, cihazınızı başlattığınızda, Key Mapper\'ı açtığınızda veya beklenmedik bir şekilde kapandığında kendini başlatır.
+ Tuş olayı eylemleri
+ Tuş olayı eylemlerinin nasıl gerçekleştirileceğini seçin
+ Acil durum ipucu
+ Güç düğmeniz çalışmazsa, PRO Modu\'nu devre dışı bırakmak için güç düğmesini 10 saniye basılı tutun ve bırakın.
+ Kurulum sihirbazı
+ Adım %d / %d
+ Etkileşimli kurulum asistanını kullan
+ Ayarlarla otomatik olarak etkileşime gir
+ Önce erişilebilirlik servisini etkinleştir
+ Eğitimi izle
+ Servisi başlat
+ Ayarlara git
+ Servisi başlat
+ Erişilebilirlik servisini etkinleştir
+ Key Mapper bu servisi kurulumda yardımcı olması için kullanır. Ayrıca sıradan tuş eşlemeleri için de gereklidir.
+ Geliştirici seçeneklerini etkinleştir
+ Key Mapper\'ın Android Hata Ayıklama Köprüsü\'nü (ADB) kullanması gerekir, bu nedenle geliştirici seçeneklerini etkinleştirmelisiniz.
+ Bir WiFi ağına bağlan
+ Key Mapper\'ın ADB\'yi etkinleştirmek için bir WiFi ağına ihtiyacı var. İnternet bağlantısına ihtiyacınız yok.\n\nWiFi ağı yok mu? Başka birinin telefonundan bir hotspot kullanın.
+ Kablosuz hata ayıklamayı etkinleştir
+ Key Mapper, yeniden eşleme ve giriş hizmetini başlatmak için kablosuz hata ayıklamayı kullanır.
+ Kablosuz hata ayıklamayı eşleştir
+ Key Mapper\'ın yeniden eşleme ve giriş hizmetini başlatabilmesi için kablosuz hata ayıklama ile eşleşmesi gerekir.
+ Servisi başlat
+ Key Mapper\'ın servisi başlatmak için ADB\'ye bağlanması gerekir.
+ Bildirimlere izin ver
+ Key Mapper\'ın kurulum sürecinde herhangi bir sorun olması durumunda sizi bilgilendirmek için izne ihtiyacı var.
+ İzin ver
+ Uyumsuz USB yapılandırması
+ PRO Modunun cihazınızı her kilitlediğinizde sonlandırılmaması için varsayılan USB yapılandırmanız olarak \'Veri aktarımı yok\' seçeneğini seçmelisiniz.
+ Kurulum asistanı
+ PRO modu çalışıyor
+ Artık ekran kapalıyken tuşları yeniden eşleyebilir ve daha fazla eylem kullanabilirsiniz.
+ Bitir
+ Geliştirici seçeneklerini etkinleştir
+ Yapı numarasına tekrar tekrar dokun
+ Otomatik olarak eşleştiriliyor
+ Eşleştirme kodu ve portu aranıyor…
+ Eşleştirme portu ve kodu bulunamadı
+ Eşleştirme koduyla eşleştirmek için düğmeye dokunun ve kodu buraya yazın
+ PRO Modu başlatılamadı
+ Tekrar kurmak için dokunun. Tekrar tekrar başarısız olursa ADB eşleştirmeyi ve telefonunuzu yeniden başlatmayı deneyin.
+ PRO modu otomatik başlatılıyor
+ Root kullanılıyor
+ Shizuku kullanılıyor
+ WiFi üzerinden ADB kullanılıyor
+ PRO modu başlatıldı
+ Yeniden eşlemede iyi eğlenceler! ❤️
+ Eşleştirme başarısız
+ Eşleştirme kodunu gönderirken eşleştirme kodu açılır penceresini ekranda tutun
+ Eşleştirme kodunu gir
+ PRO modu ile ne yapabilirim?
+ 📲 Güç düğmeniz gibi daha fazla düğmeyi yeniden eşleyebilirsiniz.\n⌨️ Tuş kodu eylemleriyle herhangi bir ekran klavyesini kullanabilirsiniz.\n⭐️ Ve daha birçok eylemi kullanabilirsiniz.
+ PRO modu bilgilerini göster
+ KapatPRO modu beklenmedik şekilde durduOtomatik olarak yeniden başlatılıyor…
- Otomatik olarak yeniden başlatılmıyor. Servisi siz durdurmuyorsanız, sorunu geliştiriciye bildirin.
+ Son otomatik başlatma 5 dakikadan daha kısa bir süre önce olduğu için otomatik olarak yeniden başlatılmıyor. Servisi siz sonlandırmıyorsanız, sorunu geliştiriciye bildirin.KeşfetNeyi yeniden eşlemek istersiniz?
@@ -1375,7 +1483,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
ÖzelÇentikKilit ekranı
- Telefonunuzu bir üst seviyeye taşıyın
+ Deneyiminizi yükseltinKayan düğmeler, herhangi bir uygulama, oyun veya kilit ekranınızdaki anlık kısayollardır. Desteğiniz Key Mapper\'ı hayatta tutar ❤️Çalışırken görünKapat
@@ -1404,6 +1512,7 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Bu cihazı yeniden eşleyebilirsinizBu özelliği kullanabilirsinizEkran kapalıyken yeniden eşle
+ Ekran kapalıyken yeniden eşleSeçeneklerGereksinimlerDüğmeleriniz hala algılanamıyorsa, lütfen Discord sunucumuza katılın ve bize bildirin. Düğmelerinizi yeniden eşlemenize yardımcı olmak için elimizden gelenin en iyisini yapacağız.
@@ -1413,10 +1522,10 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
EtkinleştirÇalışıyorSatın alındı
- PRO modu
- Ücretsiz etkinleştir
- Çalışıyor
- Bu Android sürümünde mevcut değil
+ PRO modu
+ Ücretsiz etkinleştir
+ Çalışıyor
+ Bu Android sürümünde mevcut değilCihaz asistanınız bir donanım düğmesiyle tetikleniyorsa, PRO moduyla ücretsiz olarak yeniden eşlemek mümkün olabilir. Tetikleyici sayfasından \'Diğer\'i seçmeyi deneyin ve talimatları izleyin.Bu tetikleyici, cihaza göre değişen bazı kurulumlar gerektirir. Lütfen\ talimatları okuyun
@@ -1426,9 +1535,32 @@ Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz.
Yön tuşu (D-Pad) düğmesiDiğer basit düğmelerYön tuşu (DPAD) düğmelerini yeniden eşlerken normal ekran klavyenizi kullanamayacaksınız.
- Key Mapper Klavyesi
- Klavyeyi etkinleştir
- Klavye seç
+ Key Mapper giriş yöntemi
+ Etkinleştir
+ SeçÇalışıyor
+
+ Tuş olayı eylemini düzelt
+ Bu eylemi kullanmak için ek adımlar vardır. Hangi yöntemi kullanmak istediğinizi seçin:
+ Key Mapper giriş yöntemi
+ Ekran klavyesi yok
+ Normal ekran klavyenizi her zaman kullanabilirsiniz
+ Uygulamalar ve oyunlarla daha iyi uyumluluk
+ Key Mapper giriş yöntemini etkinleştir
+ Key Mapper giriş yöntemini kullan
+ Yazarken normal ekran klavyesine otomatik olarak geç
+ Bunu istediğiniz zaman Ayarlar\'dan değiştirebilirsiniz
+ Kurulum
+ Seçenekler
+ SMS Testi
+ Başarıyla gönderildi!
+ SMS göndermek operatör veya dolaşım ücretlerine neden olabilir. Key Mapper geliştiricileri herhangi bir maliyetten sorumlu değildir.
+ Açıklama
+ Zaman aşımı
+
+ Kayan düğmeleri beğendiniz mi?
+ Görüşlerinizi merak ediyoruz! Lütfen başkalarının da bu özelliği keşfetmesine yardımcı olmak için Google Play\'de bir yorum bırakmayı düşünün. ❤️
+ Kapat
+ Evdev olayı yazılamadı
diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml
index 4286af0b29..1d70f24dac 100644
--- a/base/src/main/res/values/strings.xml
+++ b/base/src/main/res/values/strings.xml
@@ -77,8 +77,8 @@
Too many fingers to perform gesture due to android limitations.Gesture duration is too high due to android limitations.You must be using the Key Mapper Input Method for DPAD triggers to work!
- PRO mode is unsupported on this Android version
- PRO mode not started!
+ Expert Mode is unsupported on this Android version
+ Expert Mode not started!Trigger device not connected!Migrate this screen off trigger
@@ -256,6 +256,12 @@
Landscape (270°)Portrait (any)Landscape (any)
+ Screen orientation
+ Physical orientation
+ Physical: Portrait
+ Physical: Landscape
+ Physical: Portrait (upside down)
+ Physical: Landscape (inverted)App playing mediaApp not playing mediaMedia is playing
@@ -401,7 +407,7 @@
Add actionTap to record trigger
- Advanced triggers
+ Record with Expert ModeNEW!DoneFix
@@ -431,9 +437,9 @@
Please grant Key Mapper root permission in your root management app, such as Magisk.Grant WRITE_SECURE_SETTINGS permission
- You will need to use PRO mode to grant this permission.
+ You will need to use Expert Mode to grant this permission.
- Your device doesn\'t seem to have an accessibility services settings page. Tap \"guide\" to read the online guide that explains how to fix this.
+ Your device doesn\'t seem to have an accessibility services settings page. You can set up Expert Mode or run the ADB command \"adb shell pm grant io.github.sds100.keymapper android.permission.WRITE_SECURE_SETTINGS\".You must hold down the keys in the order that they are listed.There is a timeout to input this trigger. You can change this timeout in the "Options" tab.How to use this trigger
@@ -482,16 +488,16 @@
Some actions and options need this permission to work. You can also get notified when there is important news about the app.Migrate this screen off trigger
- We\'re sorry for the disruption. We\'ve introduced a *free* feature called PRO mode that replaces the old screen off remapping option. This feature is much more reliable and unlocks remapping the power button too.\n\nDue to the way it works you will need to record this trigger again with PRO mode.
+ We\'re sorry for the disruption. We\'ve introduced a *free* feature called Expert Mode that replaces the old screen off remapping option. This feature is much more reliable and unlocks remapping the power button too.\n\nDue to the way it works you will need to record this trigger again with Expert Mode.ProceedGrant READ_LOGS permission
- You will need to be rooted or have enabled PRO mode for this to work. The system will then close the app when it grants this permission. You will need to open it again yourself and try sharing the logcat again.
+ You will need to be rooted or have enabled Expert Mode for this to work. The system will then close the app when it grants this permission. You will need to open it again yourself and try sharing the logcat again.DoneKill
- Guide
+ Use Expert ModeChangeFix partiallyOk
@@ -588,7 +594,7 @@
Change automatic backup locationTurn on automatic backup
- Periodically back up your key maps
+ Back up after modifying key mapsAutomatically change the on-screen keyboard when a device (e.g a keyboard) connects/disconnectsThe Key Mapper Input Method will be automatically selected when a chosen device is connected. Your normal keyboard will be automatically selected when the device disconnects.
@@ -677,8 +683,8 @@
LoggingThis may add latency to your key maps so only turn this on if you are trying to debug the app or have been asked to by the developer.
- Use PRO mode
- Advanced detection of key events and more
+ Use Expert Mode
+ Advanced detection of key events and moreLightDark
@@ -888,7 +894,7 @@
UI element not found!Command timed out after %1$d seconds
- PRO Mode needs starting
+ Expert Mode needs startingRate limit reached. You can only send once per second.
@@ -1138,14 +1144,15 @@
Shell command actionScriptScript cannot be empty!
+ Do not put \'adb shell\'!Run as rootStandardRootADBExecution ModeADB mode does not support streaming output
- Setup PRO Mode
- Setup PRO Mode (Unsupported)
+ Setup Expert Mode
+ Setup Expert Mode (Unsupported)ConfigurationOutputNo output yet. Click Test to run the command.
@@ -1230,6 +1237,21 @@
Special
+
+ Apps
+ Media
+ Bluetooth
+ Display
+ Flashlight
+ WiFi
+ Keyboard
+ Lock
+ Phone
+ Power
+ Device
+ Time
+
+
BooleanBoolean array
@@ -1366,8 +1388,8 @@
Something went wrong 😕RetryContact developer
- You must purchase the side key trigger feature! Tap on the key map and then purchase it by clicking on \'Advanced triggers\'.
- You must purchase the floating buttons feature! Tap on the key map and then purchase it by clicking on \'Advanced triggers\'.
+ You must purchase the side key trigger feature! Tap on the key map and then purchase it by clicking on the shop.
+ You must purchase the floating buttons feature! Tap on the key map and then purchase it by clicking on the shop.contact@keymapper.clubKey Mapper Pro queryPlease 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:
@@ -1409,19 +1431,19 @@
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)Deleted floating button
- Better Caps Lock compatibility
- PRO mode provides better compatibility for Caps Lock remapping. Tap \'Use PRO mode\' and record it again.
- Use PRO mode
- Screen off remapping?
- Any trigger can now work when the screen is off for free with PRO mode! You will need to record it again.
- Use PRO mode
+ Better Caps Lock compatibility
+ Expert Mode provides better compatibility for Caps Lock remapping. Tap \'Use Expert Mode\' and record it again.
+ Use Expert Mode
+ Screen off remapping?
+ Any trigger can now work when the screen is off for free with Expert Mode! You will need to record it again.
+ Use Expert ModeApp pinning warningUsing the back button as a trigger may conflict with app pinning, causing a black screen on unlock. A reboot will fix it.Keyboard iconThe ⌨ symbol means you must use the Key Mapper input method for this trigger to work due to an Android restriction.Ringer mode actions
- Consider using PRO mode for ringer mode actions to avoid conflicts with Do Not Disturb settings.
- Use PRO mode
+ Consider using Expert Mode for ringer mode actions to avoid conflicts with Do Not Disturb settings.
+ Use Expert ModeLimit to specific apps?Add constraints in the Constraints tab.Choose a layout
@@ -1470,7 +1492,7 @@
Yes, deleteCancelHide floating layouts
- You can find floating buttons in the Advanced Triggers button when creating a trigger.
+ You can find floating buttons in the Shop button when creating a trigger.Floating buttonsMenuSort
@@ -1555,7 +1577,6 @@
Swipe down fingerprint readerSwipe left fingerprint readerSwipe right fingerprint reader
- Advanced triggersKey code %dScan code %dScan code
@@ -1564,7 +1585,7 @@
Use key code %dUse scan code %dNo scan code saved
- Use PRO mode
+ Use Expert ModeAdd more
@@ -1600,6 +1621,7 @@
OptionsUnsupported
+ UnsupportedAdd constraints if you want key maps to only work in some situations.
@@ -1630,115 +1652,116 @@
-
- PRO mode
- Important!
- Remapping buttons with PRO mode is dangerous and can cause them to stop working if you remap them incorrectly.\n\nIf you make a mistake, you may need to force restart your device by holding down the power and volume buttons for 30 seconds — consult your device\'s manual or the internet for how to do this.
- %d…
- I understand
- Understood
- Set up
- Root detected
- You can skip the set up process by giving Key Mapper root permission. This will let Key Mapper auto start PRO mode on boot as well.
- Start PRO mode
- Shizuku detected
- You can skip the set up process by giving Key Mapper Shizuku permission.
- Start Shizuku
- Request permission
- Start PRO mode
- Set up with Key Mapper
- Continue
- Continue (Android 11+)
- Options
- Enable PRO mode for all key maps
- Key Mapper will use the ADB Shell for remapping
- These settings are unavailable until you acknowledge the warning.
- PRO mode service is running
- Stop
-
- Automatically start at boot
- PRO Mode will start itself whenever you turn on or restart your device
+
+ Expert Mode
+ Setup
+ Important!
+ Remapping buttons with Expert Mode is dangerous and can cause them to stop working if you remap them incorrectly.\n\nIf you make a mistake, you may need to force restart your device by holding down the power and volume buttons for 30 seconds — consult your device\'s manual or the internet for how to do this.
+ %d…
+ I understand
+ Understood
+ Set up
+ Root detected
+ You can skip the set up process by giving Key Mapper root permission. This will let Key Mapper auto start Expert Mode without having to wait for a WiFi connection as well.
+ Start Expert Mode
+ Shizuku detected
+ You can skip the set up process by giving Key Mapper Shizuku permission.
+ Start Shizuku
+ Request permission
+ Start Expert Mode
+ Set up with Key Mapper
+ Continue
+ (Requires Android 11+)
+ Options
+ Enable Expert Mode for all key maps
+ Key Mapper will use the ADB Shell for remapping
+ These settings are unavailable until you acknowledge the warning.
+ Expert Mode service is running
+ Stop
+
+ Auto start and keep alive
+ Expert Mode will start itself whenever you boot your device or it dies unexpectedly.Key event actionsSelect how key event actions are performed
- Emergency tip
- If your power button stops working, hold down the power button for 10 seconds and release to disable PRO Mode.
+ Emergency tip
+ If your power button stops working, hold down the power button for 10 seconds and release to disable Expert Mode.
- Setup wizard
- Step %d of %d
- Use interactive setup assistant
- Automatically interact with settings
- Enable accessibility service first
- Watch tutorial
- Start service
- Go to settings
- Start service
+ Setup wizard
+ Step %d of %d
+ Use interactive setup assistant
+ Automatically interact with settings
+ Enable accessibility service first
+ Watch tutorial
+ Start service
+ Go to settings
+ Start service
- Enable accessibility service
- Key Mapper uses a service to help you set up PRO mode. It\'s also useful for ordinary key maps.
+ Enable accessibility service
+ Key Mapper uses this service to help with setup. It\'s also needed for ordinary key maps.
- Enable developer options
- Key Mapper needs to use Android Debug Bridge to start PRO mode, and you need to enable developer options for that.
+ Enable developer options
+ Key Mapper needs to use Android Debug Bridge (ADB) so you need to enable developer options.
- Connect to a WiFi network
- Key Mapper needs a WiFi network to enable ADB. You do not need an internet connection.\n\nNo WiFi network? Use a hotspot from someone else\'s phone.
+ Connect to a WiFi network
+ Key Mapper needs a WiFi network to enable ADB. You do not need an internet connection.\n\nNo WiFi network? Use a hotspot from someone else\'s phone.
- Enable wireless debugging
- Key Mapper uses wireless debugging to launch its remapping and input service.
+ Enable wireless debugging
+ Key Mapper uses wireless debugging to launch its remapping and input service.
- Pair wireless debugging
- Key Mapper needs to pair with wireless debugging before it can launch its remapping and input service.
+ Pair wireless debugging
+ Key Mapper needs to pair with wireless debugging before it can launch its remapping and input service.
- Start service
- Key Mapper needs to connect to the Android Debug Bridge to start the PRO mode service.
+ Start service
+ Key Mapper needs to connect to ADB to start the service.
- Allow notifications
- Key Mapper needs permission to notify you if there are any issues with the set up process.
- Give permission
+ Allow notifications
+ Key Mapper needs permission to notify you if there are any issues with the set up process.
+ Give permission
- Incompatible USB configuration
- You must select \'No data transfer\' as your default USB configuration so that PRO Mode is not killed every time you lock your device.
+ Incompatible USB configuration
+ You must select \'No data transfer\' as your default USB configuration so that Expert Mode is not killed every time you lock your device.
- Setup assistant
+ Setup assistant
- PRO mode is running
- You can now remap buttons when the screen is off and use more actions.
+ Expert Mode is running
+ You can now remap buttons when the screen is off and use more actions.
- Finish
+ Finish
- Enable developer options
- Tap build number repeatedly
+ Enable developer options
+ Tap build number repeatedly
- Pairing automatically
- Searching for pairing code and port…
+ Pairing automatically
+ Searching for pairing code and port…
- Unable to find pairing port and code
- Tap on the button to pair with pairing code and type the code in here
+ Unable to find pairing port and code
+ Tap on the button to pair with pairing code and type the code in here
- Starting PRO Mode failed
- Tap to set up again. Try ADB pairing and rebooting your phone if it repeatedly fails.
+ Starting Expert Mode failed
+ Tap to set up again. Try ADB pairing and rebooting your phone if it repeatedly fails.
- Auto starting PRO mode
- Using root
- Using shizuku
- Using ADB over WiFi
+ Auto starting Expert Mode
+ Using root
+ Using shizuku
+ Using ADB over WiFi
- PRO mode started
- Have fun remapping! ❤️
+ Expert Mode started
+ Have fun remapping! ❤️
- Pairing failed
- Keep the pairing code popup on-screen when submitting the pairing code
- Input pairing code
+ Pairing failed
+ Keep the pairing code popup on-screen when submitting the pairing code
+ Input pairing code
- What can I do with PRO mode?
- 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: WiFi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device.
- Show PRO mode info
- Dismiss
+ What can I do with Expert Mode?
+ 📲 You can remap more buttons, such as your power button.\n⌨️ Use any on-screen keyboard with key code actions.\n⭐️ And use many more actions.
+ Show Expert Mode info
+ Dismiss
- PRO mode stopped unexpectedly
+ Expert Mode stopped unexpectedlyAutomatically restarting…
- Not auto restarting. If you\'re not killing the service report the issue to the developer.
+ 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.Discover
@@ -1799,16 +1822,16 @@
EnableRunningPurchased
- PRO mode
- Enable for free
- Running
- Not available on this Android version
- If your device assistant is triggered by a hardware button, it might be possible to remap it for free with PRO mode. Try choosing \'Other\' from the trigger page and follow the instructions.
+ Expert Mode
+ Enable for free
+ Running
+ Not available on this Android version
+ If your device assistant is triggered by a hardware button, it might be possible to remap it for free with Expert Mode. Try choosing \'Other\' from the trigger page and follow the instructions.This trigger requires some set up that varies per device. Please\ read the instructionsRead instructionsSelect trigger type
- If your device assistant is triggered by the power button, it might be possible to remap the assistant instead, meaning you don\'t need to use PRO mode. Try choosing \'Assistant\' from the trigger page and follow the instructions.
+ If your device assistant is triggered by the power button, it might be possible to remap the assistant instead, meaning you don\'t need to use Expert Mode. Try choosing \'Assistant\' from the trigger page and follow the instructions.D-Pad buttonOther simple buttonsYou will not be able to use your normal on-screen keyboard when remapping DPAD buttons.
@@ -1823,8 +1846,8 @@
There are extra steps to use this action. Select which method you want to use:Key Mapper input methodNo on-screen keyboard
- Can use your normal on-screen keyboard at all times
- Better compatibility with apps and games
+ Can use your normal on-screen keyboard at all times
+ Better compatibility with apps and gamesEnable Key Mapper input methodUse Key Mapper input methodAutomatically switch to normal on-screen keyboard when typing
@@ -1842,5 +1865,6 @@
Are you enjoying floating buttons?We would love to know! Please consider leaving a review on Google Play to help others discover this feature. ❤️Dismiss
+ Write evdev event failed
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModelTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModelTest.kt
new file mode 100644
index 0000000000..558dfe2361
--- /dev/null
+++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModelTest.kt
@@ -0,0 +1,267 @@
+package io.github.sds100.keymapper.base.actions
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import io.github.sds100.keymapper.base.R
+import io.github.sds100.keymapper.base.repositories.FakePreferenceRepository
+import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
+import io.github.sds100.keymapper.base.utils.ui.FakeResourceProvider
+import io.github.sds100.keymapper.common.models.ShellExecutionMode
+import io.github.sds100.keymapper.common.models.ShellResult
+import io.github.sds100.keymapper.common.utils.Success
+import io.github.sds100.keymapper.data.Keys
+import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.nullValue
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@Suppress("UnusedFlow")
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner::class)
+class ConfigShellCommandViewModelTest {
+
+ @get:Rule
+ var instantExecutorRule = InstantTaskExecutorRule()
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+
+ private lateinit var viewModel: ConfigShellCommandViewModel
+ private lateinit var mockExecuteShellCommandUseCase: ExecuteShellCommandUseCase
+ private lateinit var mockNavigationProvider: NavigationProvider
+ private lateinit var mockSystemBridgeConnectionManager: SystemBridgeConnectionManager
+ private lateinit var fakePreferenceRepository: FakePreferenceRepository
+ private lateinit var fakeResourceProvider: FakeResourceProvider
+
+ private val commandEmptyErrorString = "Command cannot be empty"
+ private val descriptionEmptyErrorString = "Description cannot be empty"
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+
+ fakeResourceProvider = FakeResourceProvider().apply {
+ stringResourceMap[R.string.action_shell_command_command_empty_error] =
+ commandEmptyErrorString
+ stringResourceMap[R.string.error_cant_be_empty] = descriptionEmptyErrorString
+ }
+
+ mockExecuteShellCommandUseCase = mock()
+ mockNavigationProvider = mock()
+ mockSystemBridgeConnectionManager = mock()
+
+ fakePreferenceRepository = FakePreferenceRepository()
+
+ viewModel = ConfigShellCommandViewModel(
+ executeShellCommandUseCase = mockExecuteShellCommandUseCase,
+ navigationProvider = mockNavigationProvider,
+ systemBridgeConnectionManager = mockSystemBridgeConnectionManager,
+ preferenceRepository = fakePreferenceRepository,
+ resourceProvider = fakeResourceProvider,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `when clicking done show error when command is blank`() = runTest {
+ viewModel.onCommandChanged("")
+
+ val result = viewModel.onDoneClick()
+
+ assertThat(result, `is`(false))
+ assertThat(viewModel.state.commandError, `is`(commandEmptyErrorString))
+ assertThat(viewModel.state.descriptionError, `is`(nullValue()))
+ verify(mockNavigationProvider, never()).popBackStackWithResult(any())
+ }
+
+ @Test
+ fun `when clicking done show error when description is blank`() = runTest {
+ viewModel.onCommandChanged("echo test")
+ viewModel.onDescriptionChanged("")
+
+ val result = viewModel.onDoneClick()
+
+ assertThat(result, `is`(false))
+ assertThat(viewModel.state.descriptionError, `is`(descriptionEmptyErrorString))
+ assertThat(viewModel.state.commandError, `is`(nullValue()))
+ verify(mockNavigationProvider, never()).popBackStackWithResult(any())
+ }
+
+ @Test
+ fun `when clicking done show error when both command and description are blank`() = runTest {
+ viewModel.onCommandChanged("")
+ viewModel.onDescriptionChanged("")
+
+ val result = viewModel.onDoneClick()
+
+ assertThat(result, `is`(false))
+ assertThat(viewModel.state.commandError, `is`(commandEmptyErrorString))
+ assertThat(viewModel.state.descriptionError, `is`(nullValue()))
+ verify(mockNavigationProvider, never()).popBackStackWithResult(any())
+ }
+
+ @Test
+ fun `when clicking done with valid command and description navigate with result`() = runTest {
+ viewModel.onCommandChanged("echo test")
+ viewModel.onDescriptionChanged("Test command")
+ viewModel.onTimeoutChanged(5)
+
+ val result = viewModel.onDoneClick()
+
+ assertThat(result, `is`(true))
+ assertThat(viewModel.state.commandError, `is`(nullValue()))
+ assertThat(viewModel.state.descriptionError, `is`(nullValue()))
+ verify(mockNavigationProvider).popBackStackWithResult(any())
+ }
+
+ @Test
+ fun `when clicking test show error when command is blank`() = runTest {
+ viewModel.onCommandChanged("")
+
+ val result = viewModel.onTestClick()
+
+ assertThat(result, `is`(false))
+ assertThat(viewModel.state.commandError, `is`(commandEmptyErrorString))
+ assertThat(viewModel.state.isRunning, `is`(false))
+ verify(
+ mockExecuteShellCommandUseCase,
+ never(),
+ ).executeWithStreamingOutput(any(), any(), any())
+ }
+
+ @Test
+ fun `when clicking test with valid command start execution`() = runTest {
+ whenever(
+ mockExecuteShellCommandUseCase.executeWithStreamingOutput(any(), any(), any()),
+ ).thenReturn(flowOf(Success(ShellResult(stdout = "", exitCode = 0))))
+
+ viewModel.onCommandChanged("echo test")
+
+ val result = viewModel.onTestClick()
+
+ advanceUntilIdle()
+
+ verify(
+ mockExecuteShellCommandUseCase,
+ times(1),
+ ).executeWithStreamingOutput(eq("echo test"), eq(ShellExecutionMode.STANDARD), eq(10000))
+ assertThat(result, `is`(true))
+ assertThat(viewModel.state.commandError, `is`(nullValue()))
+ assertThat(viewModel.state.isRunning, `is`(false))
+ }
+
+ @Test
+ fun `when changing description clear error`() = runTest {
+ viewModel.onDescriptionChanged("")
+ viewModel.onCommandChanged("ls")
+ viewModel.onDoneClick()
+ assertThat(viewModel.state.descriptionError, `is`(descriptionEmptyErrorString))
+
+ viewModel.onDescriptionChanged("New description")
+
+ assertThat(viewModel.state.descriptionError, `is`(nullValue()))
+ assertThat(viewModel.state.description, `is`("New description"))
+ }
+
+ @Test
+ fun `when changing description update description`() = runTest {
+ viewModel.onDescriptionChanged("Test description")
+
+ assertThat(viewModel.state.description, `is`("Test description"))
+ assertThat(viewModel.state.descriptionError, `is`(nullValue()))
+ }
+
+ @Test
+ fun `when changing command clear error`() = runTest {
+ viewModel.onCommandChanged("")
+ viewModel.onTestClick()
+ assertThat(viewModel.state.commandError, `is`(commandEmptyErrorString))
+
+ viewModel.onCommandChanged("echo test")
+
+ assertThat(viewModel.state.commandError, `is`(nullValue()))
+ assertThat(viewModel.state.command, `is`("echo test"))
+ }
+
+ @Test
+ fun `when changing command update command`() = runTest {
+ assertThat(viewModel.state.command, `is`(""))
+
+ viewModel.onCommandChanged("echo hello")
+
+ assertThat(viewModel.state.command, `is`("echo hello"))
+ assertThat(viewModel.state.commandError, `is`(nullValue()))
+ }
+
+ @Test
+ fun `when changing command save script text to preferences`() = runTest {
+ val testCommand = "echo test command"
+
+ viewModel.onCommandChanged(testCommand)
+
+ val savedScriptText = fakePreferenceRepository.get(Keys.shellCommandScriptText)
+ .first()
+ assertThat(savedScriptText, `is`(org.hamcrest.Matchers.notNullValue()))
+ }
+
+ @Test
+ fun `when clicking done show error when command is whitespace`() = runTest {
+ viewModel.onCommandChanged(" ")
+
+ val result = viewModel.onDoneClick()
+
+ assertThat(result, `is`(false))
+ assertThat(viewModel.state.commandError, `is`(commandEmptyErrorString))
+ verify(mockNavigationProvider, never()).popBackStackWithResult(any())
+ }
+
+ @Test
+ fun `when clicking test show error when command is whitespace`() = runTest {
+ viewModel.onCommandChanged(" ")
+
+ val result = viewModel.onTestClick()
+
+ assertThat(result, `is`(false))
+ assertThat(viewModel.state.commandError, `is`(commandEmptyErrorString))
+ verify(
+ mockExecuteShellCommandUseCase,
+ never(),
+ ).executeWithStreamingOutput(any(), any(), any())
+ }
+
+ @Test
+ fun `when clicking done show error when description is whitespace`() = runTest {
+ viewModel.onCommandChanged("echo test")
+ viewModel.onDescriptionChanged(" ")
+
+ val result = viewModel.onDoneClick()
+
+ assertThat(result, `is`(false))
+ assertThat(viewModel.state.descriptionError, `is`(descriptionEmptyErrorString))
+ verify(mockNavigationProvider, never()).popBackStackWithResult(any())
+ }
+}
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt
index 71c4cd37f2..c1e2cfc7eb 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt
@@ -96,7 +96,7 @@ class GetActionErrorUseCaseTest {
val connectionState = if (isSystemBridgeConnected) {
SystemBridgeConnectionState.Connected(time = 0L)
} else {
- SystemBridgeConnectionState.Disconnected(time = 0L, isExpected = true)
+ SystemBridgeConnectionState.Disconnected(time = 0L, isStoppedByUser = true)
}
whenever(mockSystemBridgeConnectionManager.connectionState).then {
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt
index 7f2151377e..3f7941a56d 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt
@@ -1,19 +1,11 @@
package io.github.sds100.keymapper.base.actions
-import android.view.InputDevice
-import android.view.KeyEvent
-import io.github.sds100.keymapper.base.input.InjectKeyEventModel
import io.github.sds100.keymapper.base.input.InputEventHub
import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService
import io.github.sds100.keymapper.base.system.devices.FakeDevicesAdapter
-import io.github.sds100.keymapper.common.utils.InputDeviceInfo
-import io.github.sds100.keymapper.common.utils.InputEventAction
import io.github.sds100.keymapper.common.utils.KMError
-import io.github.sds100.keymapper.common.utils.State
-import io.github.sds100.keymapper.common.utils.Success
import io.github.sds100.keymapper.system.popup.ToastAdapter
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -26,7 +18,6 @@ import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
-import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -48,9 +39,7 @@ class PerformActionsUseCaseTest {
fakeDevicesAdapter = FakeDevicesAdapter()
mockAccessibilityService = mock()
mockToastAdapter = mock()
- mockInputEventHub = mock {
- on { runBlocking { injectKeyEvent(any(), any()) } }.then { Success(Unit) }
- }
+ mockInputEventHub = mock()
useCase = PerformActionsUseCaseImpl(
service = mockAccessibilityService,
@@ -113,249 +102,4 @@ class PerformActionsUseCaseTest {
// THEN
verify(mockToastAdapter, never()).show(anyOrNull())
}
-
- /**
- * issue #772
- */
- @Test
- fun `set the device id of key event actions to a connected game controller if is a game pad key code`() =
- runTest(testDispatcher) {
- // GIVEN
- val fakeGamePad = InputDeviceInfo(
- descriptor = "game_pad",
- name = "Game pad",
- id = 1,
- isExternal = true,
- isGameController = true,
- sources = InputDevice.SOURCE_GAMEPAD,
- )
-
- fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad))
-
- val action = ActionData.InputKeyEvent(
- keyCode = KeyEvent.KEYCODE_BUTTON_A,
- device = null,
- )
-
- // WHEN
- useCase.perform(action)
-
- // THEN
- val expectedDownEvent = InjectKeyEventModel(
- keyCode = KeyEvent.KEYCODE_BUTTON_A,
- action = KeyEvent.ACTION_DOWN,
- metaState = 0,
- deviceId = fakeGamePad.id,
- scanCode = 0,
- repeatCount = 0,
- source = InputDevice.SOURCE_GAMEPAD,
- )
-
- val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
-
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
- }
-
- /**
- * issue #772
- */
- @Test
- fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() =
- runTest(testDispatcher) {
- // GIVEN
- fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList())
-
- val action = ActionData.InputKeyEvent(
- keyCode = KeyEvent.KEYCODE_BUTTON_A,
- device = null,
- )
-
- // WHEN
- useCase.perform(action)
-
- // THEN
- val expectedDownEvent = InjectKeyEventModel(
-
- keyCode = KeyEvent.KEYCODE_BUTTON_A,
- action = KeyEvent.ACTION_DOWN,
- metaState = 0,
- deviceId = 0,
- scanCode = 0,
- repeatCount = 0,
- source = InputDevice.SOURCE_GAMEPAD,
- )
-
- val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
-
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
- }
-
- /**
- * issue #772
- */
- @Test
- fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() =
- runTest(testDispatcher) {
- // GIVEN
- val fakeGamePad = InputDeviceInfo(
- descriptor = "game_pad",
- name = "Game pad",
- id = 1,
- isExternal = true,
- isGameController = true,
- sources = InputDevice.SOURCE_GAMEPAD,
- )
-
- val fakeKeyboard = InputDeviceInfo(
- descriptor = "keyboard",
- name = "Keyboard",
- id = 2,
- isExternal = true,
- isGameController = false,
- sources = InputDevice.SOURCE_GAMEPAD,
- )
-
- fakeDevicesAdapter.connectedInputDevices.value =
- State.Data(listOf(fakeGamePad, fakeKeyboard))
-
- val action = ActionData.InputKeyEvent(
- keyCode = KeyEvent.KEYCODE_BUTTON_A,
- device = ActionData.InputKeyEvent.Device(
- descriptor = "keyboard",
- name = "Keyboard",
- ),
- )
-
- // WHEN
- useCase.perform(action)
-
- // THEN
- val expectedDownEvent = InjectKeyEventModel(
-
- keyCode = KeyEvent.KEYCODE_BUTTON_A,
- action = KeyEvent.ACTION_DOWN,
- metaState = 0,
- deviceId = fakeKeyboard.id,
- scanCode = 0,
- repeatCount = 0,
- source = InputDevice.SOURCE_GAMEPAD,
- )
-
- val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
-
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
- }
-
- /**
- * issue #637
- */
- @Test
- fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() =
- runTest(testDispatcher) {
- // GIVEN
- val descriptor = "fake_device_descriptor"
-
- val action = ActionData.InputKeyEvent(
- keyCode = 1,
- metaState = 0,
- device = ActionData.InputKeyEvent.Device(
- descriptor = descriptor,
- name = "fake_name_2",
- ),
- )
-
- fakeDevicesAdapter.connectedInputDevices.value = State.Data(
- listOf(
- InputDeviceInfo(
- descriptor = descriptor,
- name = "fake_name_1",
- id = 10,
- isExternal = true,
- isGameController = false,
- sources = InputDevice.SOURCE_GAMEPAD,
- ),
-
- InputDeviceInfo(
- descriptor = descriptor,
- name = "fake_name_2",
- id = 11,
- isExternal = true,
- isGameController = false,
- sources = InputDevice.SOURCE_GAMEPAD,
- ),
- ),
- )
-
- // none of the devices support the key code
- fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false }
-
- // WHEN
- useCase.perform(action, inputEventAction = InputEventAction.DOWN_UP, keyMetaState = 0)
-
- // THEN
- val expectedDownEvent = InjectKeyEventModel(
-
- keyCode = 1,
- action = KeyEvent.ACTION_DOWN,
- metaState = 0,
- deviceId = 11,
- scanCode = 0,
- repeatCount = 0,
- source = InputDevice.SOURCE_KEYBOARD,
- )
-
- val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
-
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
- }
-
- @Test
- fun `perform key event action with no device name, ensure action is still performed with correct device id`() =
- runTest(testDispatcher) {
- // GIVEN
- val descriptor = "fake_device_descriptor"
-
- val action = ActionData.InputKeyEvent(
- keyCode = 1,
- metaState = 0,
- device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""),
- )
-
- fakeDevicesAdapter.connectedInputDevices.value = State.Data(
- listOf(
- InputDeviceInfo(
- descriptor = descriptor,
- name = "fake_name",
- id = 10,
- isExternal = true,
- isGameController = false,
- sources = InputDevice.SOURCE_GAMEPAD,
- ),
- ),
- )
-
- // WHEN
- useCase.perform(action, inputEventAction = InputEventAction.DOWN_UP, keyMetaState = 0)
-
- // THEN
- val expectedDownEvent = InjectKeyEventModel(
-
- keyCode = 1,
- action = KeyEvent.ACTION_DOWN,
- metaState = 0,
- deviceId = 10,
- scanCode = 0,
- repeatCount = 0,
- source = InputDevice.SOURCE_KEYBOARD,
- )
-
- val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
-
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
- verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
- }
}
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
new file mode 100644
index 0000000000..9cb56943b0
--- /dev/null
+++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformKeyEventActionDelegateTest.kt
@@ -0,0 +1,577 @@
+package io.github.sds100.keymapper.base.actions
+
+import android.view.InputDevice
+import android.view.KeyEvent
+import io.github.sds100.keymapper.base.input.InjectKeyEventModel
+import io.github.sds100.keymapper.base.input.InputEventHub
+import io.github.sds100.keymapper.base.repositories.FakePreferenceRepository
+import io.github.sds100.keymapper.base.system.devices.FakeDevicesAdapter
+import io.github.sds100.keymapper.common.models.EvdevDeviceInfo
+import io.github.sds100.keymapper.common.utils.InputDeviceInfo
+import io.github.sds100.keymapper.common.utils.InputEventAction
+import io.github.sds100.keymapper.common.utils.State
+import io.github.sds100.keymapper.common.utils.Success
+import io.github.sds100.keymapper.data.Keys
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner::class)
+class PerformKeyEventActionDelegateTest {
+
+ companion object {
+ private val FAKE_CONTROLLER_EVDEV_DEVICE = EvdevDeviceInfo(
+ name = "Fake Controller",
+ bus = 1,
+ vendor = 2,
+ product = 1,
+ )
+ }
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testCoroutineScope = TestScope(testDispatcher)
+ private lateinit var delegate: PerformKeyEventActionDelegate
+ private lateinit var fakePreferenceRepository: FakePreferenceRepository
+ private lateinit var mockInputEventHub: InputEventHub
+
+ private lateinit var fakeDevicesAdapter: FakeDevicesAdapter
+
+ @Before
+ fun init() {
+ fakePreferenceRepository = FakePreferenceRepository()
+ fakeDevicesAdapter = FakeDevicesAdapter()
+
+ mockInputEventHub = mock {
+ on { runBlocking { injectKeyEvent(any(), any()) } }.then { Success(Unit) }
+ on {
+ runBlocking { injectEvdevEventKeyCode(any(), any(), any()) }
+ }.then { Success(Unit) }
+ }
+
+ delegate = PerformKeyEventActionDelegate(
+ coroutineScope = testCoroutineScope,
+ settingsRepository = fakePreferenceRepository,
+ inputEventHub = mockInputEventHub,
+ devicesAdapter = fakeDevicesAdapter,
+ )
+ }
+
+ @Test
+ fun `inject evdev event if action device set as a non-evdev device but it is disconnected`() =
+ runTest(testDispatcher) {
+ fakePreferenceRepository.set(Keys.keyEventActionsUseSystemBridge, true)
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = ActionData.InputKeyEvent.Device(
+ descriptor = "keyboard_descriptor",
+ name = "Keyboard",
+ ),
+ )
+
+ // The Keyboard is not connected, so still perform the action through the evdev device
+ // that triggered it.
+ fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList())
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Evdev(deviceId = 1),
+ )
+
+ verify(mockInputEventHub).injectEvdevEventKeyCode(
+ deviceId = 1,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ value = 1,
+ )
+
+ verify(mockInputEventHub).injectEvdevEventKeyCode(
+ deviceId = 1,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ value = 0,
+ )
+ }
+
+ @Test
+ fun `do not inject evdev event if triggered by evdev device and action device is set`() =
+ runTest(testDispatcher) {
+ fakePreferenceRepository.set(Keys.keyEventActionsUseSystemBridge, true)
+
+ fakeDevicesAdapter.connectedInputDevices.value = State.Data(
+ listOf(
+ InputDeviceInfo(
+ descriptor = "keyboard_descriptor",
+ name = "Keyboard",
+ id = 10,
+ isExternal = true,
+ isGameController = false,
+ sources = InputDevice.SOURCE_KEYBOARD,
+ ),
+ ),
+ )
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_A,
+ device = ActionData.InputKeyEvent.Device(
+ descriptor = "keyboard_descriptor",
+ name = "Keyboard",
+ ),
+ )
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Evdev(deviceId = 1),
+ )
+
+ val expectedDownEvent = InjectKeyEventModel(
+ keyCode = KeyEvent.KEYCODE_A,
+ action = KeyEvent.ACTION_DOWN,
+ metaState = 0,
+ deviceId = 10,
+ scanCode = 0,
+ repeatCount = 0,
+ source = InputDevice.SOURCE_KEYBOARD,
+ )
+
+ val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
+
+ verify(mockInputEventHub).injectKeyEvent(expectedDownEvent, true)
+ verify(mockInputEventHub).injectKeyEvent(expectedUpEvent, true)
+ verify(mockInputEventHub, never()).injectEvdevEventKeyCode(any(), any(), any())
+ }
+
+ @Test
+ fun `inject evdev event if triggered by evdev device and action device set to the same device`() =
+ runTest(testDispatcher) {
+ fakePreferenceRepository.set(Keys.keyEventActionsUseSystemBridge, true)
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = ActionData.InputKeyEvent.Device(
+ descriptor = "descriptor",
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ ),
+ )
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Evdev(deviceId = 1),
+ )
+
+ verify(mockInputEventHub).injectEvdevEventKeyCode(
+ deviceId = 1,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ value = 1,
+ )
+
+ verify(mockInputEventHub).injectEvdevEventKeyCode(
+ deviceId = 1,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ value = 0,
+ )
+ }
+
+ @Test
+ fun `inject evdev event if triggered by evdev device and action device not set`() =
+ runTest(testDispatcher) {
+ fakePreferenceRepository.set(Keys.keyEventActionsUseSystemBridge, true)
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = null,
+ )
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
+
+ verify(mockInputEventHub).injectEvdevEventKeyCode(
+ deviceId = 0,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ value = 1,
+ )
+
+ verify(mockInputEventHub).injectEvdevEventKeyCode(
+ deviceId = 0,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ value = 0,
+ )
+ }
+
+ @Test
+ fun `do not inject evdev event if not using system bridge for key event actions`() =
+ runTest(testDispatcher) {
+ fakePreferenceRepository.set(Keys.keyEventActionsUseSystemBridge, false)
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = null,
+ )
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
+
+ val expectedDownEvent = InjectKeyEventModel(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ action = KeyEvent.ACTION_DOWN,
+ metaState = 0,
+ deviceId = 0,
+ scanCode = 0,
+ repeatCount = 0,
+ source = InputDevice.SOURCE_GAMEPAD,
+ )
+
+ val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
+
+ verify(mockInputEventHub).injectKeyEvent(expectedDownEvent, false)
+ verify(mockInputEventHub).injectKeyEvent(expectedUpEvent, false)
+ verify(mockInputEventHub, never()).injectEvdevEventKeyCode(any(), any(), any())
+ }
+
+ @Test
+ fun `inject down evdev event if triggered by evdev device and action device not set`() =
+ runTest(testDispatcher) {
+ fakePreferenceRepository.set(Keys.keyEventActionsUseSystemBridge, true)
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = null,
+ )
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
+
+ verify(mockInputEventHub).injectEvdevEventKeyCode(
+ deviceId = 0,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ value = 1,
+ )
+ }
+
+ @Test
+ fun `inject up evdev event if triggered by evdev device and action device not set`() =
+ runTest(testDispatcher) {
+ fakePreferenceRepository.set(Keys.keyEventActionsUseSystemBridge, true)
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = null,
+ )
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
+
+ verify(mockInputEventHub).injectEvdevEventKeyCode(
+ deviceId = 0,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ value = 0,
+ )
+ }
+
+ /**
+ * issue #772
+ */
+ @Test
+ fun `set the device id of key event actions to a connected game controller if is a game pad key code`() =
+ runTest(testDispatcher) {
+ // GIVEN
+ val fakeGamePad = InputDeviceInfo(
+ descriptor = "game_pad",
+ name = "Game pad",
+ id = 1,
+ isExternal = true,
+ isGameController = true,
+ sources = InputDevice.SOURCE_GAMEPAD,
+ )
+
+ fakeDevicesAdapter.connectedInputDevices.value =
+ State.Data(listOf(fakeGamePad))
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = null,
+ )
+
+ // WHEN
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Default,
+ )
+
+ // THEN
+ val expectedDownEvent = InjectKeyEventModel(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ action = KeyEvent.ACTION_DOWN,
+ metaState = 0,
+ deviceId = fakeGamePad.id,
+ scanCode = 0,
+ repeatCount = 0,
+ source = InputDevice.SOURCE_GAMEPAD,
+ )
+
+ val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
+
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
+ }
+
+ /**
+ * issue #772
+ */
+ @Test
+ fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() =
+ runTest(testDispatcher) {
+ // GIVEN
+ fakeDevicesAdapter.connectedInputDevices.value =
+ State.Data(emptyList())
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = null,
+ )
+
+ // WHEN
+
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Default,
+ )
+
+ // THEN
+ val expectedDownEvent = InjectKeyEventModel(
+
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ action = KeyEvent.ACTION_DOWN,
+ metaState = 0,
+ deviceId = 0,
+ scanCode = 0,
+ repeatCount = 0,
+ source = InputDevice.SOURCE_GAMEPAD,
+ )
+
+ val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
+
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
+ }
+
+ /**
+ * issue #772
+ */
+ @Test
+ fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() =
+ runTest(testDispatcher) {
+ // GIVEN
+ val fakeGamePad = InputDeviceInfo(
+ descriptor = "game_pad",
+ name = "Game pad",
+ id = 1,
+ isExternal = true,
+ isGameController = true,
+ sources = InputDevice.SOURCE_GAMEPAD,
+ )
+
+ val fakeKeyboard = InputDeviceInfo(
+ descriptor = "keyboard",
+ name = "Keyboard",
+ id = 2,
+ isExternal = true,
+ isGameController = false,
+ sources = InputDevice.SOURCE_GAMEPAD,
+ )
+
+ fakeDevicesAdapter.connectedInputDevices.value =
+ State.Data(listOf(fakeGamePad, fakeKeyboard))
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ device = ActionData.InputKeyEvent.Device(
+ descriptor = "keyboard",
+ name = "Keyboard",
+ ),
+ )
+
+ // WHEN
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Default,
+ )
+
+ // THEN
+ val expectedDownEvent = InjectKeyEventModel(
+
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ action = KeyEvent.ACTION_DOWN,
+ metaState = 0,
+ deviceId = fakeKeyboard.id,
+ scanCode = 0,
+ repeatCount = 0,
+ source = InputDevice.SOURCE_GAMEPAD,
+ )
+
+ val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
+
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
+ }
+
+ /**
+ * issue #637
+ */
+ @Test
+ fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() =
+ runTest(testDispatcher) {
+ // GIVEN
+ val descriptor = "fake_device_descriptor"
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = 1,
+ metaState = 0,
+ device = ActionData.InputKeyEvent.Device(
+ descriptor = descriptor,
+ name = "fake_name_2",
+ ),
+ )
+
+ fakeDevicesAdapter.connectedInputDevices.value =
+ State.Data(
+ listOf(
+ InputDeviceInfo(
+ descriptor = descriptor,
+ name = "fake_name_1",
+ id = 10,
+ isExternal = true,
+ isGameController = false,
+ sources = InputDevice.SOURCE_GAMEPAD,
+ ),
+
+ InputDeviceInfo(
+ descriptor = descriptor,
+ name = "fake_name_2",
+ id = 11,
+ isExternal = true,
+ isGameController = false,
+ sources = InputDevice.SOURCE_GAMEPAD,
+ ),
+ ),
+ )
+
+ // none of the devices support the key code
+ fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false }
+
+ // WHEN
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Default,
+ )
+
+ // THEN
+ val expectedDownEvent = InjectKeyEventModel(
+
+ keyCode = 1,
+ action = KeyEvent.ACTION_DOWN,
+ metaState = 0,
+ deviceId = 11,
+ scanCode = 0,
+ repeatCount = 0,
+ source = InputDevice.SOURCE_KEYBOARD,
+ )
+
+ val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
+
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
+ }
+
+ @Test
+ fun `perform key event action with no device name, ensure action is still performed with correct device id`() =
+ runTest(testDispatcher) {
+ // GIVEN
+ val descriptor = "fake_device_descriptor"
+
+ val action = ActionData.InputKeyEvent(
+ keyCode = 1,
+ metaState = 0,
+ device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""),
+ )
+
+ fakeDevicesAdapter.connectedInputDevices.value = State.Data(
+ listOf(
+ InputDeviceInfo(
+ descriptor = descriptor,
+ name = "fake_name",
+ id = 10,
+ isExternal = true,
+ isGameController = false,
+ sources = InputDevice.SOURCE_GAMEPAD,
+ ),
+ ),
+ )
+
+ // WHEN
+ delegate.perform(
+ action,
+ inputEventAction = InputEventAction.DOWN_UP,
+ keyMetaState = 0,
+ triggerDevice = PerformActionTriggerDevice.Default,
+ )
+
+ // THEN
+ val expectedDownEvent = InjectKeyEventModel(
+
+ keyCode = 1,
+ action = KeyEvent.ACTION_DOWN,
+ metaState = 0,
+ deviceId = 10,
+ scanCode = 0,
+ repeatCount = 0,
+ source = InputDevice.SOURCE_KEYBOARD,
+ )
+
+ val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP)
+
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent, false)
+ verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent, false)
+ }
+}
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionControllerTest.kt
new file mode 100644
index 0000000000..dccbc31835
--- /dev/null
+++ b/base/src/test/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionControllerTest.kt
@@ -0,0 +1,1116 @@
+package io.github.sds100.keymapper.base.detection
+
+import android.view.KeyEvent
+import io.github.sds100.keymapper.base.actions.Action
+import io.github.sds100.keymapper.base.actions.ActionData
+import io.github.sds100.keymapper.base.keymaps.ClickType
+import io.github.sds100.keymapper.base.keymaps.KeyMap
+import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey
+import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice
+import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey
+import io.github.sds100.keymapper.base.trigger.Trigger
+import io.github.sds100.keymapper.base.utils.parallelTrigger
+import io.github.sds100.keymapper.base.utils.singleKeyTrigger
+import io.github.sds100.keymapper.common.models.EvdevDeviceInfo
+import io.github.sds100.keymapper.common.models.GrabTargetKeyCode
+import io.github.sds100.keymapper.system.inputevents.Scancode
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.collection.IsEmptyCollection.empty
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.MockedStatic
+import org.mockito.Mockito.mockStatic
+import org.mockito.kotlin.mock
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class KeyMapDetectionControllerTest {
+
+ companion object {
+ private val FAKE_CONTROLLER_EVDEV_DEVICE = EvdevDeviceInfo(
+ name = "Fake Controller",
+ bus = 1,
+ vendor = 2,
+ product = 1,
+ )
+
+ private val FAKE_CONTROLLER_EVDEV_DEVICE_2 = EvdevDeviceInfo(
+ name = "Fake Controller 2",
+ bus = 1,
+ vendor = 3,
+ product = 2,
+ )
+ }
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private lateinit var algorithm: KeyMapAlgorithm
+ private lateinit var mockedKeyEvent: MockedStatic
+
+ @Before
+ fun init() {
+ mockedKeyEvent = mockStatic(KeyEvent::class.java)
+ mockedKeyEvent.`when` { KeyEvent.getMaxKeyCode() }.thenReturn(1000)
+
+ algorithm = KeyMapAlgorithm(
+ coroutineScope = testScope,
+ useCase = mock(),
+ performActionsUseCase = mock(),
+ detectConstraints = mock(),
+ )
+ }
+
+ @After
+ fun tearDown() {
+ mockedKeyEvent.close()
+ }
+
+ @Test
+ fun `Only grab the same device once if multiple key maps add key events`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ Action(data = ActionData.OpenCamera),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X),
+ ),
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_Y),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(
+ KeyEvent.KEYCODE_BUTTON_X,
+ KeyEvent.KEYCODE_BUTTON_Y,
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Grab multiple evdev devices from multiple triggers with key event actions`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ Action(data = ActionData.OpenCamera),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X),
+ ),
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ ),
+ ),
+ actionList = listOf(
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_Y),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(KeyEvent.KEYCODE_BUTTON_X),
+ ),
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ extraKeyCodes = intArrayOf(KeyEvent.KEYCODE_BUTTON_Y),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Do not grab evdev devices with extra key codes if key event actions do not use system bridge`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ Action(data = ActionData.OpenCamera),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X),
+ ),
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ ),
+ ),
+ actionList = listOf(
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_Y),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(
+ algorithm,
+ injectKeyEventActionsWithSystemBridge = false,
+ )
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(),
+ ),
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Grab multiple evdev devices from multiple triggers`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Grab multiple evdev devices from the same trigger`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = parallelTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Grab evdev device for evdev trigger`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Do not grab evdev device if key map has no actions`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = emptyList(),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+ assertThat(grabRequests, empty())
+ }
+
+ @Test
+ fun `Grab evdev device with extra key codes if action inputs key events`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_B)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(KeyEvent.KEYCODE_BUTTON_B),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Do not grab evdev device for key event trigger`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ KeyEventTriggerKey(
+ keyCode = KeyEvent.KEYCODE_VOLUME_DOWN,
+ clickType = ClickType.SHORT_PRESS,
+ device = KeyEventTriggerDevice.Any,
+ ),
+ ),
+ ),
+ )
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+ assertThat(grabRequests, empty())
+ }
+
+ @Test
+ fun `do not grab any evdev devices if no triggers`() {
+ algorithm.loadKeyMaps(emptyList())
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+ assertThat(grabRequests, empty())
+ }
+
+ // ==================== TRIGGER KEY COMBINATION EDGE CASES ====================
+
+ @Test
+ fun `Same key on different devices in parallel trigger should grab both devices`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = parallelTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Long press trigger should grab device`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.LONG_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Double press trigger should grab device`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.DOUBLE_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Same key with different click types in different key maps should grab device once`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.LONG_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.GoHome)),
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.DOUBLE_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.GoBack)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Mixed evdev and key event triggers in parallel should only grab evdev device`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = parallelTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ KeyEventTriggerKey(
+ keyCode = KeyEvent.KEYCODE_VOLUME_DOWN,
+ clickType = ClickType.SHORT_PRESS,
+ device = KeyEventTriggerDevice.Any,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ // ==================== ACTION EDGE CASES ====================
+
+ @Test
+ fun `Multiple key event actions should add all key codes to extra key codes`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_Y),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_L1),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(
+ KeyEvent.KEYCODE_BUTTON_X,
+ KeyEvent.KEYCODE_BUTTON_Y,
+ KeyEvent.KEYCODE_BUTTON_L1,
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Duplicate key event actions should not duplicate extra key codes`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(KeyEvent.KEYCODE_BUTTON_X),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Action that inputs same key code as trigger key should include in extra key codes`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_A),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(KeyEvent.KEYCODE_BUTTON_A),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Key map with only non-key-event actions should have empty extra key codes`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ Action(data = ActionData.OpenCamera),
+ Action(data = ActionData.GoHome),
+ Action(data = ActionData.Screenshot),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Key event action with meta state should include key code in extra key codes`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ Action(
+ data = ActionData.InputKeyEvent(
+ keyCode = KeyEvent.KEYCODE_C,
+ metaState = KeyEvent.META_CTRL_ON,
+ ),
+ ),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(KeyEvent.KEYCODE_C),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Mixed key event and non-key-event actions should only include key codes from key event actions`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(
+ Action(data = ActionData.OpenCamera),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X),
+ Action(data = ActionData.GoHome),
+ buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_Y),
+ Action(data = ActionData.Screenshot),
+ ),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(
+ KeyEvent.KEYCODE_BUTTON_X,
+ KeyEvent.KEYCODE_BUTTON_Y,
+ ),
+ ),
+ ),
+ )
+ }
+
+ // ==================== DEVICE HANDLING EDGE CASES ====================
+
+ @Test
+ fun `Disabled key map should not produce grab requests`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ isEnabled = false,
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(grabRequests, empty())
+ }
+
+ @Test
+ fun `Mix of enabled and disabled key maps should only grab for enabled ones`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ isEnabled = true,
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.GoHome)),
+ isEnabled = false,
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Disabled key map with key event actions should not contribute extra key codes`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X)),
+ isEnabled = true,
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_Y)),
+ isEnabled = false,
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(KeyEvent.KEYCODE_BUTTON_X),
+ ),
+ ),
+ )
+ }
+
+ // ==================== EMPTY/NULL AND DUPLICATE HANDLING ====================
+
+ @Test
+ fun `Key map with empty trigger keys should not produce grab requests`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = Trigger(keys = emptyList()),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(grabRequests, empty())
+ }
+
+ @Test
+ fun `Same extra key code from multiple key maps on same device should not duplicate`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X)),
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(buildKeyEventAction(KeyEvent.KEYCODE_BUTTON_X)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = intArrayOf(KeyEvent.KEYCODE_BUTTON_X),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `All key maps disabled should produce empty grab requests`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ isEnabled = false,
+ ),
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE_2,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.GoHome)),
+ isEnabled = false,
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(grabRequests, empty())
+ }
+
+ @Test
+ fun `Large number of key maps should correctly aggregate grab requests`() {
+ val keyMaps = (0 until 10).map { index ->
+ KeyMap(
+ trigger = singleKeyTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A + index,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A + index,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(buildKeyEventAction(KeyEvent.KEYCODE_0 + index)),
+ )
+ }
+ loadKeyMaps(*keyMaps.toTypedArray())
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ val expectedExtraKeyCodes = (0 until 10).map { KeyEvent.KEYCODE_0 + it }.toIntArray()
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ extraKeyCodes = expectedExtraKeyCodes,
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Parallel trigger with multiple keys on same device should grab once`() {
+ loadKeyMaps(
+ KeyMap(
+ trigger = parallelTrigger(
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_A,
+ keyCode = KeyEvent.KEYCODE_BUTTON_A,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_B,
+ keyCode = KeyEvent.KEYCODE_BUTTON_B,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ EvdevTriggerKey(
+ scanCode = Scancode.BTN_X,
+ keyCode = KeyEvent.KEYCODE_BUTTON_X,
+ clickType = ClickType.SHORT_PRESS,
+ device = FAKE_CONTROLLER_EVDEV_DEVICE,
+ ),
+ ),
+ actionList = listOf(Action(data = ActionData.OpenCamera)),
+ ),
+ )
+
+ val grabRequests = KeyMapDetectionController.getEvdevGrabRequests(algorithm)
+
+ assertThat(
+ grabRequests,
+ contains(
+ GrabTargetKeyCode(
+ name = FAKE_CONTROLLER_EVDEV_DEVICE.name,
+ bus = FAKE_CONTROLLER_EVDEV_DEVICE.bus,
+ vendor = FAKE_CONTROLLER_EVDEV_DEVICE.vendor,
+ product = FAKE_CONTROLLER_EVDEV_DEVICE.product,
+ extraKeyCodes = intArrayOf(),
+ ),
+ ),
+ )
+ }
+
+ private fun buildKeyEventAction(keyCode: Int): Action {
+ return Action(
+ data = ActionData.InputKeyEvent(keyCode = keyCode),
+ )
+ }
+
+ private fun loadKeyMaps(vararg keyMap: KeyMap) {
+ val models = listOf(*keyMap).map { DetectKeyMapModel(it) }
+ algorithm.loadKeyMaps(models)
+ }
+}
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
new file mode 100644
index 0000000000..7161d21886
--- /dev/null
+++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt
@@ -0,0 +1,704 @@
+package io.github.sds100.keymapper.base.expertmode
+
+import io.github.sds100.keymapper.base.R
+import io.github.sds100.keymapper.base.repositories.FakePreferenceRepository
+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.data.Keys
+import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
+import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
+import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController
+import io.github.sds100.keymapper.system.network.NetworkAdapter
+import io.github.sds100.keymapper.system.notifications.NotificationAdapter
+import io.github.sds100.keymapper.system.notifications.NotificationModel
+import io.github.sds100.keymapper.system.permissions.Permission
+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 kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.atLeast
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.inOrder
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner::class)
+class SystemBridgeAutoStarterTest {
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testCoroutineScope = TestScope(testDispatcher)
+
+ private lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter
+
+ private lateinit var testScopeClock: TestScopeClock
+ private lateinit var mockSuAdapter: SuAdapter
+ private lateinit var mockShizukuAdapter: ShizukuAdapter
+ private lateinit var mockConnectionManager: SystemBridgeConnectionManager
+ private lateinit var mockSetupController: SystemBridgeSetupController
+ private lateinit var fakePreferences: FakePreferenceRepository
+ private lateinit var mockNetworkAdapter: NetworkAdapter
+ private lateinit var mockPermissionAdapter: PermissionAdapter
+ private lateinit var mockNotificationAdapter: NotificationAdapter
+ private lateinit var mockResourceProvider: ResourceProvider
+ private lateinit var testBuildConfig: TestBuildConfigProvider
+
+ private lateinit var isRootGrantedFlow: MutableStateFlow
+ private lateinit var shizukuIsStartedFlow: MutableStateFlow
+ private lateinit var connectionStateFlow: MutableStateFlow
+ private lateinit var isWifiConnectedFlow: MutableStateFlow
+ private lateinit var writeSecureSettingsGrantedFlow: MutableStateFlow
+ private lateinit var shizukuPermissionGrantedFlow: MutableStateFlow
+
+ @Before
+ fun init() {
+ isRootGrantedFlow = MutableStateFlow(false)
+ shizukuIsStartedFlow = MutableStateFlow(false)
+ connectionStateFlow = MutableStateFlow(
+ SystemBridgeConnectionState.Disconnected(
+ time = testCoroutineScope.testScheduler.currentTime,
+ isStoppedByUser = false,
+ ),
+ )
+ isWifiConnectedFlow = MutableStateFlow(false)
+ writeSecureSettingsGrantedFlow = MutableStateFlow(false)
+ shizukuPermissionGrantedFlow = MutableStateFlow(false)
+
+ mockSuAdapter = mock {
+ on { isRootGranted } doReturn isRootGrantedFlow
+ }
+
+ mockShizukuAdapter = mock {
+ on { isStarted } doReturn shizukuIsStartedFlow
+ }
+
+ mockConnectionManager = mock {
+ on { connectionState } doReturn connectionStateFlow
+ }
+
+ mockSetupController = mock()
+ runBlocking {
+ whenever(mockSetupController.isAdbPaired()).doReturn(false)
+ }
+
+ fakePreferences = FakePreferenceRepository()
+
+ mockNetworkAdapter = mock {
+ on { isWifiConnected } doReturn isWifiConnectedFlow
+ }
+
+ mockPermissionAdapter = mock {
+ on { isGrantedFlow(Permission.WRITE_SECURE_SETTINGS) } doReturn
+ writeSecureSettingsGrantedFlow
+ on { isGrantedFlow(Permission.SHIZUKU) } doReturn shizukuPermissionGrantedFlow
+ }
+
+ mockNotificationAdapter = mock()
+
+ mockResourceProvider = mock {
+ on { getString(any()) } doReturn "test_string"
+ }
+
+ testScopeClock = TestScopeClock(testCoroutineScope)
+
+ testBuildConfig = TestBuildConfigProvider(sdkInt = 30)
+
+ systemBridgeAutoStarter = SystemBridgeAutoStarter(
+ coroutineScope = testCoroutineScope,
+ clock = testScopeClock,
+ suAdapter = mockSuAdapter,
+ shizukuAdapter = mockShizukuAdapter,
+ connectionManager = mockConnectionManager,
+ setupController = mockSetupController,
+ preferences = fakePreferences,
+ networkAdapter = mockNetworkAdapter,
+ permissionAdapter = mockPermissionAdapter,
+ notificationAdapter = mockNotificationAdapter,
+ resourceProvider = mockResourceProvider,
+ buildConfig = testBuildConfig,
+ )
+ }
+
+ @Test
+ fun `auto start within 60 seconds of booting`() = 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()
+
+ verify(mockSetupController).autoStartWithAdb()
+ }
+
+ @Test
+ fun `auto start 5 minutes after booting`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ whenever(mockSetupController.isAdbPaired()).thenReturn(true)
+ isWifiConnectedFlow.value = true
+ writeSecureSettingsGrantedFlow.value = true
+
+ advanceTimeBy(310_000)
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockSetupController).autoStartWithAdb()
+ }
+
+ @Test
+ fun `do not auto start with ADB if WRITE_SECURE_SETTINGS is denied`() =
+ runTest(testDispatcher) {
+ advanceTimeBy(5000L)
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+
+ writeSecureSettingsGrantedFlow.value = false
+ isWifiConnectedFlow.value = false
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockSetupController, never()).autoStartWithAdb()
+ }
+
+ @Test
+ fun `do not auto start with ADB if wifi is disconnected`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ writeSecureSettingsGrantedFlow.value = true
+ isWifiConnectedFlow.value = false
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockSetupController, never()).autoStartWithAdb()
+ }
+
+ @Test
+ fun `auto start with shizuku on Android 10`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ testBuildConfig.sdkInt = 29
+ isRootGrantedFlow.value = false
+ shizukuIsStartedFlow.value = true
+ shizukuPermissionGrantedFlow.value = true
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithShizuku()
+ }
+
+ @Test
+ fun `auto start with root on Android 9`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ testBuildConfig.sdkInt = 28
+ isRootGrantedFlow.value = true
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithRoot()
+ }
+
+ @Test
+ fun `auto start from pre version 4 when rooted and it was never used before`() =
+ runTest(testDispatcher) {
+ isRootGrantedFlow.value = true
+ fakePreferences.set(Keys.isSystemBridgeUsed, null)
+ fakePreferences.set(Keys.handledUpgradeToExpertMode, null)
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithRoot()
+ }
+
+ @Test
+ fun `auto start from pre version 4 when shizuku is started and it was never used before`() =
+ runTest(testDispatcher) {
+ shizukuIsStartedFlow.value = true
+ shizukuPermissionGrantedFlow.value = true
+ fakePreferences.set(Keys.isSystemBridgeUsed, null)
+ fakePreferences.set(Keys.handledUpgradeToExpertMode, null)
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithShizuku()
+ }
+
+ @Test
+ fun `set key event actions to use system bridge when upgrading to version 4 and rooted`() =
+ runTest(testDispatcher) {
+ isRootGrantedFlow.value = true
+ fakePreferences.set(Keys.isSystemBridgeUsed, null)
+ fakePreferences.set(Keys.handledUpgradeToExpertMode, null)
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ assertThat(fakePreferences.get(Keys.keyEventActionsUseSystemBridge).first(), `is`(true))
+ }
+
+ @Test
+ fun `set key event actions to use system bridge when upgrading to version 4 and shizuku is started`() =
+ runTest(testDispatcher) {
+ shizukuIsStartedFlow.value = true
+ shizukuPermissionGrantedFlow.value = true
+ fakePreferences.set(Keys.isSystemBridgeUsed, null)
+ fakePreferences.set(Keys.handledUpgradeToExpertMode, null)
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ assertThat(fakePreferences.get(Keys.keyEventActionsUseSystemBridge).first(), `is`(true))
+ }
+
+ @Test
+ fun `prioritize auto starting with root if shizuku also started`() = runTest(testDispatcher) {
+ advanceTimeBy(1_000_000L)
+ isRootGrantedFlow.value = true
+ shizukuIsStartedFlow.value = true
+ shizukuPermissionGrantedFlow.value = true
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithRoot()
+ }
+
+ @Test
+ fun `do not auto start from pre version 4 when shizuku permission granted but not started`() =
+ runTest(testDispatcher) {
+ advanceTimeBy(1_000_000L)
+ shizukuIsStartedFlow.value = false
+ shizukuPermissionGrantedFlow.value = true
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager, never()).startWithShizuku()
+ }
+
+ @Test
+ fun `auto start with root`() = runTest(testDispatcher) {
+ advanceTimeBy(1_000_000L)
+ isRootGrantedFlow.value = true
+ fakePreferences.set(Keys.isSystemBridgeEmergencyKilled, false)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithRoot()
+ }
+
+ @Test
+ fun `do not auto start when emergency killed`() = runTest(testDispatcher) {
+ isRootGrantedFlow.value = true
+ fakePreferences.set(Keys.isSystemBridgeEmergencyKilled, true)
+ fakePreferences.set(Keys.handledUpgradeToExpertMode, true)
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager, never()).startWithRoot()
+ verify(mockConnectionManager, never()).startWithShizuku()
+ verify(mockSetupController, never()).autoStartWithAdb()
+ }
+
+ @Test
+ fun `auto start with shizuku when shizuku is available`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+ shizukuIsStartedFlow.value = true
+ shizukuPermissionGrantedFlow.value = true
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithShizuku()
+ }
+
+ @Test
+ fun `auto start with Shizuku if Shizuku takes a minute to connect`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ // Initially Shizuku is disconnected, and is connected 50 seconds after booting
+ shizukuIsStartedFlow.value = false
+ shizukuPermissionGrantedFlow.value = true
+
+ inOrder(mockConnectionManager) {
+ systemBridgeAutoStarter.init()
+ advanceTimeBy(50000)
+ verify(mockConnectionManager, never()).startWithShizuku()
+
+ // Shizuku starts
+ shizukuIsStartedFlow.value = true
+
+ advanceTimeBy(20000)
+ verify(mockConnectionManager).startWithShizuku()
+ }
+ }
+
+ @Test
+ fun `auto start with ADB if WiFi takes a minute to connect`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ whenever(mockSetupController.isAdbPaired()).thenReturn(true)
+ writeSecureSettingsGrantedFlow.value = true
+
+ // Initially the wifi is disconnected, and is connected 50 seconds after booting
+ isWifiConnectedFlow.value = false
+
+ inOrder(mockSetupController) {
+ systemBridgeAutoStarter.init()
+ advanceTimeBy(50000)
+ verify(mockSetupController, never()).autoStartWithAdb()
+
+ // Connect to Wi-Fi network
+ isWifiConnectedFlow.value = true
+
+ advanceTimeBy(20000)
+ verify(mockSetupController).autoStartWithAdb()
+ }
+ }
+
+ @Test
+ fun `do not auto start when auto start is disabled`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, false)
+ fakePreferences.set(Keys.handledUpgradeToExpertMode, true)
+ isRootGrantedFlow.value = true
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager, never()).startWithRoot()
+ verify(mockConnectionManager, never()).startWithShizuku()
+ verify(mockSetupController, never()).autoStartWithAdb()
+ }
+
+ @Test
+ fun `do not auto start when already connected`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ systemBridgeAutoStarter.init()
+ advanceTimeBy(5000)
+
+ // The system bridge connects during init()
+ connectionStateFlow.value = SystemBridgeConnectionState.Connected(time = 1000)
+
+ advanceUntilIdle()
+
+ verify(mockConnectionManager, never()).startWithRoot()
+ verify(mockConnectionManager, never()).startWithShizuku()
+ verify(mockSetupController, never()).autoStartWithAdb()
+ }
+
+ @Test
+ fun `show notification when auto starting`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ whenever(mockSetupController.isAdbPaired()).thenReturn(true)
+ isWifiConnectedFlow.value = true
+ writeSecureSettingsGrantedFlow.value = true
+
+ inOrder(mockNotificationAdapter) {
+ systemBridgeAutoStarter.init()
+ advanceTimeBy(10000)
+
+ // Show the notification that it is auto starting
+ verify(mockNotificationAdapter).showNotification(any())
+
+ // Set the state as connected within the timeout
+ connectionStateFlow.value = SystemBridgeConnectionState.Connected(time = 10000)
+ advanceUntilIdle()
+ // Do not show another notification after the timeout
+ verify(mockNotificationAdapter, never()).showNotification(any())
+ }
+ }
+
+ @Test
+ fun `show failed notification when connection times out`() = runTest(testDispatcher) {
+ isRootGrantedFlow.value = true
+ fakePreferences.set(Keys.isSystemBridgeEmergencyKilled, false)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+ connectionStateFlow.value =
+ SystemBridgeConnectionState.Disconnected(time = 0, isStoppedByUser = false)
+
+ systemBridgeAutoStarter.init()
+ // The system bridge remains disconnected
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithRoot()
+ verify(mockNotificationAdapter, atLeast(1)).showNotification(any())
+ }
+
+ @Test
+ fun `do not auto start when connected`() = runTest(testDispatcher) {
+ advanceTimeBy(1_000_000L)
+ val connectedState = SystemBridgeConnectionState.Connected(time = 1000)
+ connectionStateFlow.value = connectedState
+ isRootGrantedFlow.value = true
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager, never()).startWithRoot()
+ verify(mockConnectionManager, never()).startWithShizuku()
+ verify(mockSetupController, never()).autoStartWithAdb()
+ }
+
+ @Test
+ fun `do not auto restart when disconnected by the user`() = runTest(testDispatcher) {
+ advanceTimeBy(5000L)
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+ isRootGrantedFlow.value = true
+
+ inOrder(mockConnectionManager, mockSetupController) {
+ systemBridgeAutoStarter.init()
+ advanceTimeBy(100_000)
+ verify(mockConnectionManager).startWithRoot()
+
+ // Disconnect the system bridge. Expected is true
+ connectionStateFlow.value = SystemBridgeConnectionState.Disconnected(
+ time = 1_000_000L,
+ isStoppedByUser = true,
+ )
+
+ advanceUntilIdle()
+
+ verify(mockConnectionManager, never()).startWithRoot()
+ verify(mockConnectionManager, never()).startWithShizuku()
+ verify(mockSetupController, never()).autoStartWithAdb()
+ }
+ }
+
+ @Test
+ fun `do not auto restart within 5 minutes of the last auto start`() = runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+ isRootGrantedFlow.value = true
+
+ inOrder(mockConnectionManager) {
+ systemBridgeAutoStarter.init()
+ advanceTimeBy(6000)
+ // Try auto starting when the process launches
+ verify(mockConnectionManager).startWithRoot()
+ connectionStateFlow.value = SystemBridgeConnectionState.Connected(time = 6000)
+
+ advanceUntilIdle()
+ // It is killed unexpectedly straight after auto starting
+ connectionStateFlow.value = SystemBridgeConnectionState.Disconnected(
+ time = 7000,
+ isStoppedByUser = true,
+ )
+
+ advanceUntilIdle()
+ // Auto starting fails and so it does not try starting again
+ verify(mockConnectionManager, never()).startWithRoot()
+ }
+ }
+
+ @Test
+ fun `show killed and not restarting notification if want to autostart again within the cooldown`() =
+ runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ whenever(
+ mockResourceProvider.getString(R.string.system_bridge_died_notification_title),
+ ).thenReturn("died")
+ whenever(mockSetupController.isAdbPaired()).thenReturn(true)
+ isWifiConnectedFlow.value = true
+ writeSecureSettingsGrantedFlow.value = true
+
+ inOrder(mockNotificationAdapter) {
+ systemBridgeAutoStarter.init()
+ advanceTimeBy(10000)
+
+ // Show the notification that it is auto starting
+ verify(mockNotificationAdapter).showNotification(any())
+ connectionStateFlow.value = SystemBridgeConnectionState.Connected(time = 10000)
+
+ // Set the state as disconnected within the timeout
+ connectionStateFlow.value =
+ SystemBridgeConnectionState.Disconnected(time = 11000, isStoppedByUser = false)
+ advanceUntilIdle()
+
+ // Show notification
+ val argument = argumentCaptor()
+ verify(mockNotificationAdapter).showNotification(argument.capture())
+
+ assertThat(argument.firstValue.title, `is`("died"))
+ }
+ }
+
+ @Test
+ fun `auto restart when shizuku connects after the system bridge dies`() =
+ runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ // Shizuku is not started initially
+ shizukuIsStartedFlow.value = false
+ shizukuPermissionGrantedFlow.value = true
+
+ // System bridge was already running when Key Mapper process launched
+ connectionStateFlow.value = SystemBridgeConnectionState.Connected(time = 0)
+
+ inOrder(mockConnectionManager) {
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ // If it dies, then it will wait for shizuku to connect
+ connectionStateFlow.value =
+ SystemBridgeConnectionState.Disconnected(time = 3000, isStoppedByUser = false)
+
+ advanceTimeBy(2000)
+ verify(mockConnectionManager, never()).startWithShizuku()
+
+ shizukuIsStartedFlow.value = true
+ advanceUntilIdle()
+
+ verify(mockConnectionManager).startWithShizuku()
+ }
+ }
+
+ @Test
+ fun `do not auto start when shizuku is connected if system bridge was stopped by the user`() =
+ runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ // Shizuku is not started initially
+ shizukuIsStartedFlow.value = false
+ shizukuPermissionGrantedFlow.value = true
+
+ // System bridge was already running when Key Mapper process launched
+ connectionStateFlow.value = SystemBridgeConnectionState.Connected(time = 0)
+
+ inOrder(mockConnectionManager) {
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ // The user stopped the system bridge
+ connectionStateFlow.value =
+ SystemBridgeConnectionState.Disconnected(time = 3000, isStoppedByUser = true)
+
+ advanceTimeBy(2000)
+ verify(mockConnectionManager, never()).startWithShizuku()
+
+ shizukuIsStartedFlow.value = true
+ advanceUntilIdle()
+
+ // Do not start it when shizuku connects
+ verify(mockConnectionManager, never()).startWithShizuku()
+ }
+ }
+
+ @Test
+ fun `auto restart when wifi is connected after the system bridge dies`() =
+ runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+
+ // WiFi is not connected initially
+ isWifiConnectedFlow.value = false
+ writeSecureSettingsGrantedFlow.value = true
+ whenever(mockSetupController.isAdbPaired()).thenReturn(true)
+
+ // System bridge was already running when Key Mapper process launched
+ connectionStateFlow.value = SystemBridgeConnectionState.Connected(time = 0)
+
+ inOrder(mockSetupController) {
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ // If it dies, then it will wait for wifi to connect
+ connectionStateFlow.value =
+ SystemBridgeConnectionState.Disconnected(time = 2000, isStoppedByUser = false)
+
+ advanceTimeBy(2000)
+ verify(mockSetupController, never()).autoStartWithAdb()
+
+ isWifiConnectedFlow.value = true
+ advanceUntilIdle()
+
+ verify(mockSetupController).autoStartWithAdb()
+ }
+ }
+
+ @Test
+ fun `do not auto start with Shizuku on launch if it was never used before`() =
+ runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeUsed, null)
+ fakePreferences.set(Keys.handledUpgradeToExpertMode, true)
+ shizukuIsStartedFlow.value = true
+ shizukuPermissionGrantedFlow.value = true
+
+ systemBridgeAutoStarter.init()
+ advanceUntilIdle()
+
+ verify(mockConnectionManager, never()).startWithRoot()
+ verify(mockConnectionManager, never()).startWithShizuku()
+ verify(mockSetupController, never()).autoStartWithAdb()
+ }
+
+ @Test
+ fun `do not auto restart if keep alive is disabled while it is connected`() =
+ runTest(testDispatcher) {
+ fakePreferences.set(Keys.isSystemBridgeUsed, true)
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true)
+ isRootGrantedFlow.value = true
+
+ inOrder(mockConnectionManager) {
+ systemBridgeAutoStarter.init()
+ advanceTimeBy(6000)
+
+ connectionStateFlow.value = SystemBridgeConnectionState.Connected(time = 6000)
+ verify(mockConnectionManager).startWithRoot()
+
+ // The user disabled keep alive while system bridge is running
+ fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, false)
+ // The system bridge dies
+ connectionStateFlow.value =
+ SystemBridgeConnectionState.Disconnected(time = 7000, isStoppedByUser = false)
+
+ advanceTimeBy(2000)
+
+ // The system bridge should not be auto restarted.
+ verify(mockConnectionManager, never()).startWithRoot()
+ }
+ }
+}
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
new file mode 100644
index 0000000000..db43edbbc6
--- /dev/null
+++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt
@@ -0,0 +1,131 @@
+package io.github.sds100.keymapper.base.expertmode
+
+import io.github.sds100.keymapper.base.repositories.FakePreferenceRepository
+import io.github.sds100.keymapper.data.Keys
+import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
+import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController
+import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter
+import io.github.sds100.keymapper.system.network.NetworkAdapter
+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 kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner::class)
+class SystemBridgeSetupUseCaseTest {
+
+ private lateinit var useCase: SystemBridgeSetupUseCaseImpl
+ private lateinit var fakePreferences: FakePreferenceRepository
+ private lateinit var mockSuAdapter: SuAdapter
+ private lateinit var mockSystemBridgeSetupController: SystemBridgeSetupController
+ private lateinit var mockSystemBridgeConnectionManager: SystemBridgeConnectionManager
+ private lateinit var mockShizukuAdapter: ShizukuAdapter
+ private lateinit var mockPermissionAdapter: PermissionAdapter
+ private lateinit var mockAccessibilityServiceAdapter: AccessibilityServiceAdapter
+ private lateinit var mockNetworkAdapter: NetworkAdapter
+
+ @Before
+ fun init() {
+ fakePreferences = FakePreferenceRepository()
+ mockSuAdapter = mock()
+ mockSystemBridgeSetupController = mock()
+ mockSystemBridgeConnectionManager = mock()
+ mockShizukuAdapter = mock()
+ mockPermissionAdapter = mock()
+ mockAccessibilityServiceAdapter = mock()
+ mockNetworkAdapter = mock()
+
+ useCase = SystemBridgeSetupUseCaseImpl(
+ preferences = fakePreferences,
+ suAdapter = mockSuAdapter,
+ systemBridgeSetupController = mockSystemBridgeSetupController,
+ systemBridgeConnectionManager = mockSystemBridgeConnectionManager,
+ shizukuAdapter = mockShizukuAdapter,
+ permissionAdapter = mockPermissionAdapter,
+ accessibilityServiceAdapter = mockAccessibilityServiceAdapter,
+ networkAdapter = mockNetworkAdapter,
+ )
+ }
+
+ @Test
+ fun `set isSystemBridgeStoppedByUser to true when stopping system bridge`() = runTest {
+ useCase.stopSystemBridge()
+
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeStoppedByUser).first(),
+ `is`(true),
+ )
+ verify(mockSystemBridgeConnectionManager).stopSystemBridge()
+ }
+
+ @Test
+ fun `set isSystemBridgeEmergencyKilled and isSystemBridgeStoppedByUser to false when starting system bridge with root`() =
+ runTest {
+ useCase.startSystemBridgeWithRoot()
+
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeEmergencyKilled).first(),
+ `is`(false),
+ )
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeStoppedByUser).first(),
+ `is`(false),
+ )
+ }
+
+ @Test
+ fun `set isSystemBridgeEmergencyKilled and isSystemBridgeStoppedByUser to false when starting system bridge with shizuku`() =
+ runTest {
+ useCase.startSystemBridgeWithShizuku()
+
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeEmergencyKilled).first(),
+ `is`(false),
+ )
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeStoppedByUser).first(),
+ `is`(false),
+ )
+ }
+
+ @Test
+ fun `set isSystemBridgeEmergencyKilled and isSystemBridgeStoppedByUser to false when starting system bridge with adb`() =
+ runTest {
+ useCase.startSystemBridgeWithAdb()
+
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeEmergencyKilled).first(),
+ `is`(false),
+ )
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeStoppedByUser).first(),
+ `is`(false),
+ )
+ }
+
+ @Test
+ fun `set isSystemBridgeEmergencyKilled and isSystemBridgeStoppedByUser to false when auto starting system bridge with adb`() =
+ runTest {
+ useCase.startSystemBridgeWithAdb()
+
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeEmergencyKilled).first(),
+ `is`(false),
+ )
+ assertThat(
+ fakePreferences.get(Keys.isSystemBridgeStoppedByUser).first(),
+ `is`(false),
+ )
+ }
+}
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DetectKeyMapsUseCaseTest.kt
similarity index 99%
rename from base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt
rename to base/src/test/java/io/github/sds100/keymapper/base/keymaps/DetectKeyMapsUseCaseTest.kt
index c20fc2682c..a5f20562cf 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DetectKeyMapsUseCaseTest.kt
@@ -11,7 +11,7 @@ import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.Test
-class ProcessKeyMapGroupsForDetectionTest {
+class DetectKeyMapsUseCaseTest {
@Test
fun `Key map in grandchild group, all have constraints, and parent does not exist then ignore key map`() {
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt
index 0cf0ab5c52..d920f854f7 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt
@@ -6,7 +6,6 @@ import io.github.sds100.keymapper.base.detection.DpadMotionEventTracker
import io.github.sds100.keymapper.common.utils.InputDeviceInfo
import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent
import io.github.sds100.keymapper.system.inputevents.KMKeyEvent
-import junitparams.JUnitParamsRunner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasItem
@@ -14,10 +13,8 @@ import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.junit.Before
import org.junit.Test
-import org.junit.runner.RunWith
@ExperimentalCoroutinesApi
-@RunWith(JUnitParamsRunner::class)
class DpadMotionEventTrackerTest {
companion object {
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 11a00e269a..8a066f8581 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
@@ -6,6 +6,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import io.github.sds100.keymapper.base.actions.Action
import io.github.sds100.keymapper.base.actions.ActionData
import io.github.sds100.keymapper.base.actions.ActionErrorSnapshot
+import io.github.sds100.keymapper.base.actions.PerformActionTriggerDevice
import io.github.sds100.keymapper.base.actions.PerformActionsUseCase
import io.github.sds100.keymapper.base.actions.RepeatMode
import io.github.sds100.keymapper.base.constraints.Constraint
@@ -30,7 +31,6 @@ import io.github.sds100.keymapper.base.utils.parallelTrigger
import io.github.sds100.keymapper.base.utils.sequenceTrigger
import io.github.sds100.keymapper.base.utils.singleKeyTrigger
import io.github.sds100.keymapper.base.utils.triggerKey
-import io.github.sds100.keymapper.common.models.EvdevDeviceHandle
import io.github.sds100.keymapper.common.models.EvdevDeviceInfo
import io.github.sds100.keymapper.common.utils.InputDeviceInfo
import io.github.sds100.keymapper.common.utils.InputEventAction
@@ -253,7 +253,10 @@ class KeyMapAlgorithmTest {
)
inputUpEvdevEvent(KeyEvent.KEYCODE_UNKNOWN, Scancode.BTN_LEFT, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -271,7 +274,13 @@ class KeyMapAlgorithmTest {
inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(
+ performActionsUseCase,
+ times(1),
+ ).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -286,10 +295,23 @@ class KeyMapAlgorithmTest {
)
loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION)))
- inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
- inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
+ inputDownEvdevEvent(
+ KeyEvent.KEYCODE_B,
+ Scancode.KEY_B,
+ FAKE_CONTROLLER_EVDEV_DEVICE,
+ deviceId = 1,
+ )
+ inputUpEvdevEvent(
+ KeyEvent.KEYCODE_B,
+ Scancode.KEY_B,
+ FAKE_CONTROLLER_EVDEV_DEVICE,
+ deviceId = 1,
+ )
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 1),
+ )
}
@Test
@@ -309,7 +331,10 @@ class KeyMapAlgorithmTest {
inputDownEvdevEvent(KeyEvent.KEYCODE_C, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
inputUpEvdevEvent(KeyEvent.KEYCODE_C, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -360,7 +385,10 @@ class KeyMapAlgorithmTest {
inputDownEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE)
inputUpEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -392,7 +420,10 @@ class KeyMapAlgorithmTest {
inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
inputUpEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -413,7 +444,10 @@ class KeyMapAlgorithmTest {
delay(LONG_PRESS_DELAY + 100L)
inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -440,7 +474,10 @@ class KeyMapAlgorithmTest {
delay(50L)
inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -546,7 +583,10 @@ class KeyMapAlgorithmTest {
inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -572,7 +612,10 @@ class KeyMapAlgorithmTest {
inputUpEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE)
inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -607,7 +650,10 @@ class KeyMapAlgorithmTest {
mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -624,7 +670,10 @@ class KeyMapAlgorithmTest {
mockEvdevKeyInput(trigger.keys[0], FAKE_VOLUME_EVDEV_DEVICE)
- verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, times(1)).perform(
+ TEST_ACTION.data,
+ device = PerformActionTriggerDevice.Evdev(deviceId = 0),
+ )
}
@Test
@@ -1691,23 +1740,23 @@ class KeyMapAlgorithmTest {
}
}
- whenever(performActionsUseCase.perform(any(), any(), any())).doAnswer {
+ whenever(performActionsUseCase.perform(any(), any(), any(), any())).doAnswer {
isFlashlightEnabled = !isFlashlightEnabled
}
inOrder(performActionsUseCase) {
// flashlight is initially disabled so don't trigger.
mockTriggerKeyInput(keyMap.trigger.keys[0])
- verify(performActionsUseCase, never()).perform(any(), any(), any())
+ verify(performActionsUseCase, never()).perform(any(), any(), any(), any())
isFlashlightEnabled = true
// trigger because flashlight is enabled. Triggering the action will disable the flashlight.
mockTriggerKeyInput(keyMap.trigger.keys[0])
- verify(performActionsUseCase, times(1)).perform(any(), any(), any())
+ verify(performActionsUseCase, times(1)).perform(any(), any(), any(), any())
// Don't trigger because the flashlight is now disabled
mockTriggerKeyInput(keyMap.trigger.keys[0])
- verify(performActionsUseCase, never()).perform(any(), any(), any())
+ verify(performActionsUseCase, never()).perform(any(), any(), any(), any())
}
}
@@ -2839,7 +2888,7 @@ class KeyMapAlgorithmTest {
mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS))
- verify(performActionsUseCase, never()).perform(any(), any(), any())
+ verify(performActionsUseCase, never()).perform(any(), any(), any(), any())
mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A))
mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS))
@@ -3096,7 +3145,7 @@ class KeyMapAlgorithmTest {
mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE))
// THEN
- verify(performActionsUseCase, never()).perform(any(), any(), any())
+ verify(performActionsUseCase, never()).perform(any(), any(), any(), any())
}
@Test
@@ -4746,43 +4795,51 @@ class KeyMapAlgorithmTest {
),
)
- private fun inputDownEvdevEvent(keyCode: Int, scanCode: Int, device: EvdevDeviceInfo): Boolean =
- controller.onInputEvent(
- KMEvdevEvent(
- type = KMEvdevEvent.TYPE_KEY_EVENT,
- device = EvdevDeviceHandle(
- path = "/dev/input${device.name}",
- name = device.name,
- bus = device.bus,
- vendor = device.vendor,
- product = device.product,
- ),
- code = scanCode,
- androidCode = keyCode,
- value = KMEvdevEvent.VALUE_DOWN,
- timeSec = testScope.currentTime,
- timeUsec = 0,
+ private fun inputDownEvdevEvent(
+ keyCode: Int,
+ scanCode: Int,
+ device: EvdevDeviceInfo,
+ deviceId: Int = 0,
+ ): Boolean = controller.onInputEvent(
+ KMEvdevEvent(
+ type = KMEvdevEvent.TYPE_KEY_EVENT,
+ deviceId = deviceId,
+ deviceInfo = EvdevDeviceInfo(
+ name = device.name,
+ bus = device.bus,
+ vendor = device.vendor,
+ product = device.product,
),
- )
+ code = scanCode,
+ androidCode = keyCode,
+ value = KMEvdevEvent.VALUE_DOWN,
+ timeSec = testScope.currentTime,
+ timeUsec = 0,
+ ),
+ )
- private fun inputUpEvdevEvent(keyCode: Int, scanCode: Int, device: EvdevDeviceInfo): Boolean =
- controller.onInputEvent(
- KMEvdevEvent(
- type = KMEvdevEvent.TYPE_KEY_EVENT,
- device = EvdevDeviceHandle(
- path = "/dev/input${device.name}",
- name = device.name,
- bus = device.bus,
- vendor = device.vendor,
- product = device.product,
- ),
- code = scanCode,
- androidCode = keyCode,
- value = KMEvdevEvent.VALUE_UP,
- timeSec = testScope.currentTime,
- timeUsec = 0,
+ private fun inputUpEvdevEvent(
+ keyCode: Int,
+ scanCode: Int,
+ device: EvdevDeviceInfo,
+ deviceId: Int = 0,
+ ): Boolean = controller.onInputEvent(
+ KMEvdevEvent(
+ type = KMEvdevEvent.TYPE_KEY_EVENT,
+ deviceId = deviceId,
+ deviceInfo = EvdevDeviceInfo(
+ name = device.name,
+ bus = device.bus,
+ vendor = device.vendor,
+ product = device.product,
),
- )
+ code = scanCode,
+ androidCode = keyCode,
+ value = KMEvdevEvent.VALUE_UP,
+ timeSec = testScope.currentTime,
+ timeUsec = 0,
+ ),
+ )
private suspend fun mockParallelTrigger(trigger: Trigger, delay: Long? = null) {
require(trigger.mode is TriggerMode.Parallel)
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt
index 8383bbc63f..4bc6370c7e 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt
@@ -11,7 +11,6 @@ import io.github.sds100.keymapper.base.detection.TriggerKeyMapFromOtherAppsContr
import io.github.sds100.keymapper.base.trigger.Trigger
import io.github.sds100.keymapper.base.utils.TestConstraintSnapshot
import io.github.sds100.keymapper.common.utils.KMError
-import junitparams.JUnitParamsRunner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -21,14 +20,12 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@ExperimentalCoroutinesApi
-@RunWith(JUnitParamsRunner::class)
class TriggerKeyMapFromOtherAppsControllerTest {
companion object {
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerViewModelTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerViewModelTest.kt
index cf6696a678..1ed7f2bc56 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerViewModelTest.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerViewModelTest.kt
@@ -9,9 +9,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is visible when system bridge is connected`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Idle,
- isProModeRecordingEnabled = false,
+ isExpertModeRecordingEnabled = false,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -20,12 +20,12 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is not visible when system bridge is disconnected`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Idle,
- isProModeRecordingEnabled = false,
+ isExpertModeRecordingEnabled = false,
systemBridgeState = SystemBridgeConnectionState.Disconnected(
time = 0L,
- isExpected = true,
+ isStoppedByUser = true,
),
)
@@ -34,12 +34,12 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is not visible when system bridge is disconnected unexpectedly`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Idle,
- isProModeRecordingEnabled = false,
+ isExpertModeRecordingEnabled = false,
systemBridgeState = SystemBridgeConnectionState.Disconnected(
time = 0L,
- isExpected = false,
+ isStoppedByUser = false,
),
)
@@ -48,9 +48,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is checked when pro mode recording is enabled`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Idle,
- isProModeRecordingEnabled = true,
+ isExpertModeRecordingEnabled = true,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -59,9 +59,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is not checked when pro mode recording is disabled`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Idle,
- isProModeRecordingEnabled = false,
+ isExpertModeRecordingEnabled = false,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -70,9 +70,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is enabled when record trigger state is idle`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Idle,
- isProModeRecordingEnabled = false,
+ isExpertModeRecordingEnabled = false,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -81,9 +81,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is enabled when record trigger state is completed`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Completed(emptyList()),
- isProModeRecordingEnabled = false,
+ isExpertModeRecordingEnabled = false,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -92,9 +92,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is disabled when record trigger state is counting down`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.CountingDown(timeLeft = 3),
- isProModeRecordingEnabled = false,
+ isExpertModeRecordingEnabled = false,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -103,9 +103,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is disabled when counting down even if pro mode recording is enabled`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.CountingDown(timeLeft = 5),
- isProModeRecordingEnabled = true,
+ isExpertModeRecordingEnabled = true,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -115,9 +115,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is visible and checked when connected and enabled`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Idle,
- isProModeRecordingEnabled = true,
+ isExpertModeRecordingEnabled = true,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -128,12 +128,12 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is not visible when disconnected even if recording is enabled`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.Idle,
- isProModeRecordingEnabled = true,
+ isExpertModeRecordingEnabled = true,
systemBridgeState = SystemBridgeConnectionState.Disconnected(
time = 0L,
- isExpected = true,
+ isStoppedByUser = true,
),
)
@@ -144,9 +144,9 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is visible but disabled when counting down and connected`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.CountingDown(timeLeft = 1),
- isProModeRecordingEnabled = true,
+ isExpertModeRecordingEnabled = true,
systemBridgeState = SystemBridgeConnectionState.Connected(time = 0L),
)
@@ -157,12 +157,12 @@ class ConfigTriggerViewModelTest {
@Test
fun `switch is not visible and disabled when counting down and disconnected`() {
- val result = BaseConfigTriggerViewModel.buildProModeSwitchState(
+ val result = BaseConfigTriggerViewModel.buildExpertModeSwitchState(
recordTriggerState = RecordTriggerState.CountingDown(timeLeft = 2),
- isProModeRecordingEnabled = false,
+ isExpertModeRecordingEnabled = false,
systemBridgeState = SystemBridgeConnectionState.Disconnected(
time = 0L,
- isExpected = false,
+ isStoppedByUser = false,
),
)
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestBuildConfigProvider.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestBuildConfigProvider.kt
index 67393a19b2..877cb35bd3 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestBuildConfigProvider.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestBuildConfigProvider.kt
@@ -4,8 +4,8 @@ import android.os.Build
import io.github.sds100.keymapper.base.BuildConfig
import io.github.sds100.keymapper.common.BuildConfigProvider
-class TestBuildConfigProvider(override val sdkInt: Int) : BuildConfigProvider {
- override val minApi: Int = Build.VERSION_CODES.LOLLIPOP
+class TestBuildConfigProvider(override var sdkInt: Int) : BuildConfigProvider {
+ override val minApi: Int = Build.VERSION_CODES.O
override val maxApi: Int = 1000
override val packageName: String = BuildConfig.LIBRARY_PACKAGE_NAME
override val version: String = "1.0.0"
diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt
index 063557172f..be77315302 100644
--- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt
+++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt
@@ -4,6 +4,7 @@ import io.github.sds100.keymapper.base.constraints.Constraint
import io.github.sds100.keymapper.base.constraints.ConstraintData
import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot
import io.github.sds100.keymapper.common.utils.Orientation
+import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo
import io.github.sds100.keymapper.system.camera.CameraLens
import io.github.sds100.keymapper.system.foldable.HingeState
@@ -17,6 +18,7 @@ class TestConstraintSnapshot(
val appInForeground: String? = null,
val connectedBluetoothDevices: Set = emptySet(),
val orientation: Orientation = Orientation.ORIENTATION_0,
+ val physicalOrientation: PhysicalOrientation = PhysicalOrientation.PORTRAIT,
val isScreenOn: Boolean = false,
val appsPlayingMedia: List = emptyList(),
val isWifiEnabled: Boolean = false,
@@ -62,6 +64,9 @@ class TestConstraintSnapshot(
orientation == Orientation.ORIENTATION_0 ||
orientation == Orientation.ORIENTATION_180
+ is ConstraintData.PhysicalOrientation ->
+ physicalOrientation == data.physicalOrientation
+
is ConstraintData.ScreenOff -> !isScreenOn
is ConstraintData.ScreenOn -> isScreenOn
is ConstraintData.FlashlightOff -> when (data.lens) {
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
new file mode 100644
index 0000000000..5cb9711011
--- /dev/null
+++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestScopeClock.kt
@@ -0,0 +1,13 @@
+package io.github.sds100.keymapper.base.utils
+
+import io.github.sds100.keymapper.common.utils.Clock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.currentTime
+
+class TestScopeClock(private val testScope: TestScope) : Clock {
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun elapsedRealtime(): Long {
+ return testScope.currentTime
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 9e070cd106..78ed67584b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,9 @@
- // Top-level build file where you can add configuration options common to all sub-projects/modules.
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
+ // Must come before the others for the jnilibs folder to be automatically detected
+ alias(libs.plugins.mozilla.rust.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.kapt) apply false
alias(libs.plugins.kotlin.serialization) apply false
diff --git a/common/src/main/aidl/ADD_TO_PROGUARD.txt b/common/src/main/aidl/ADD_TO_PROGUARD.txt
new file mode 100644
index 0000000000..c89cbd2713
--- /dev/null
+++ b/common/src/main/aidl/ADD_TO_PROGUARD.txt
@@ -0,0 +1 @@
+do not forget to add exemptions for AIDL files to proguard
\ No newline at end of file
diff --git a/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl b/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.aidl
similarity index 63%
rename from common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl
rename to common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.aidl
index 8ea6d7f489..daeafbd464 100644
--- a/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl
+++ b/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.aidl
@@ -1,3 +1,3 @@
package io.github.sds100.keymapper.common.models;
-parcelable EvdevDeviceHandle;
\ No newline at end of file
+parcelable EvdevDeviceInfo;
\ No newline at end of file
diff --git a/common/src/main/aidl/io/github/sds100/keymapper/common/models/GrabTargetKeyCode.aidl b/common/src/main/aidl/io/github/sds100/keymapper/common/models/GrabTargetKeyCode.aidl
new file mode 100644
index 0000000000..1959d02203
--- /dev/null
+++ b/common/src/main/aidl/io/github/sds100/keymapper/common/models/GrabTargetKeyCode.aidl
@@ -0,0 +1,3 @@
+package io.github.sds100.keymapper.common.models;
+
+parcelable GrabTargetKeyCode;
diff --git a/common/src/main/aidl/io/github/sds100/keymapper/common/models/GrabbedDeviceHandle.aidl b/common/src/main/aidl/io/github/sds100/keymapper/common/models/GrabbedDeviceHandle.aidl
new file mode 100644
index 0000000000..2ad7314522
--- /dev/null
+++ b/common/src/main/aidl/io/github/sds100/keymapper/common/models/GrabbedDeviceHandle.aidl
@@ -0,0 +1,3 @@
+package io.github.sds100.keymapper.common.models;
+
+parcelable GrabbedDeviceHandle;
\ No newline at end of file
diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/common/models/ShellResult.aidl b/common/src/main/aidl/io/github/sds100/keymapper/common/models/ShellResult.aidl
similarity index 100%
rename from sysbridge/src/main/aidl/io/github/sds100/keymapper/common/models/ShellResult.aidl
rename to common/src/main/aidl/io/github/sds100/keymapper/common/models/ShellResult.aidl
diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/GrabDeviceRequest.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/GrabDeviceRequest.kt
new file mode 100644
index 0000000000..b1d07c5d7a
--- /dev/null
+++ b/common/src/main/java/io/github/sds100/keymapper/common/models/GrabDeviceRequest.kt
@@ -0,0 +1,46 @@
+package io.github.sds100.keymapper.common.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+
+@Serializable
+@Parcelize
+data class GrabDeviceRequest(
+ val name: String,
+ val bus: Int,
+ val vendor: Int,
+ val product: Int,
+ /**
+ * The Android key codes to add support for from this device.
+ *
+ * One can request the duplicated uinput device to support extra key codes that the
+ * original device doesn't support. This is needed when a key map for this device
+ * inputs a key code that isn't supported by the original evdev device.
+ */
+ val extraKeyCodes: IntArray,
+) : Parcelable {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GrabDeviceRequest
+
+ if (name != other.name) return false
+ if (bus != other.bus) return false
+ if (vendor != other.vendor) return false
+ if (product != other.product) return false
+ if (!extraKeyCodes.contentEquals(other.extraKeyCodes)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = name.hashCode()
+ result = 31 * result + bus.hashCode()
+ result = 31 * result + vendor.hashCode()
+ result = 31 * result + product.hashCode()
+ result = 31 * result + extraKeyCodes.contentHashCode()
+ return result
+ }
+}
diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/GrabTargetKeyCode.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/GrabTargetKeyCode.kt
new file mode 100644
index 0000000000..c1e436d9a7
--- /dev/null
+++ b/common/src/main/java/io/github/sds100/keymapper/common/models/GrabTargetKeyCode.kt
@@ -0,0 +1,58 @@
+package io.github.sds100.keymapper.common.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+
+@Serializable
+@Parcelize
+data class GrabTargetKeyCode(
+ val name: String,
+ val bus: Int,
+ val vendor: Int,
+ val product: Int,
+ /**
+ * The Android key codes to add support for from this device.
+ *
+ * One can request the duplicated uinput device to support extra key codes that the
+ * original device doesn't support. This is needed when a key map for this device
+ * inputs a key code that isn't supported by the original evdev device.
+ */
+ val extraKeyCodes: IntArray,
+) : Parcelable {
+
+ constructor(
+ device: EvdevDeviceInfo,
+ extraKeyCodes: IntArray,
+ ) : this(
+ device.name,
+ device.bus,
+ device.vendor,
+ device.product,
+ extraKeyCodes,
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GrabTargetKeyCode
+
+ if (name != other.name) return false
+ if (bus != other.bus) return false
+ if (vendor != other.vendor) return false
+ if (product != other.product) return false
+ if (!extraKeyCodes.contentEquals(other.extraKeyCodes)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = name.hashCode()
+ result = 31 * result + bus.hashCode()
+ result = 31 * result + vendor.hashCode()
+ result = 31 * result + product.hashCode()
+ result = 31 * result + extraKeyCodes.contentHashCode()
+ return result
+ }
+}
diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/GrabbedDeviceHandle.kt
similarity index 64%
rename from common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt
rename to common/src/main/java/io/github/sds100/keymapper/common/models/GrabbedDeviceHandle.kt
index 1b14c03b86..a9d1855a2d 100644
--- a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt
+++ b/common/src/main/java/io/github/sds100/keymapper/common/models/GrabbedDeviceHandle.kt
@@ -4,11 +4,8 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
-data class EvdevDeviceHandle(
- /**
- * The path to the device. E.g /dev/input/event1
- */
- val path: String,
+data class GrabbedDeviceHandle(
+ val id: Int,
val name: String,
val bus: Int,
val vendor: Int,
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
new file mode 100644
index 0000000000..cd98addbd5
--- /dev/null
+++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/Clock.kt
@@ -0,0 +1,16 @@
+package io.github.sds100.keymapper.common.utils
+
+import android.os.SystemClock
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ClockImpl @Inject constructor() : Clock {
+ override fun elapsedRealtime(): Long {
+ return SystemClock.elapsedRealtime()
+ }
+}
+
+interface Clock {
+ fun elapsedRealtime(): Long
+}
diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/Constants.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/Constants.kt
index 9ba395cf9c..d06a8fe6fc 100644
--- a/common/src/main/java/io/github/sds100/keymapper/common/utils/Constants.kt
+++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/Constants.kt
@@ -3,7 +3,7 @@ package io.github.sds100.keymapper.common.utils
import android.os.Build
object Constants {
- const val MIN_API: Int = Build.VERSION_CODES.LOLLIPOP
+ const val MIN_API: Int = Build.VERSION_CODES.O
const val MAX_API: Int = 1000
const val SYSTEM_BRIDGE_MIN_API = Build.VERSION_CODES.Q
}
diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/PhysicalOrientation.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/PhysicalOrientation.kt
new file mode 100644
index 0000000000..a89aa9a2d2
--- /dev/null
+++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/PhysicalOrientation.kt
@@ -0,0 +1,12 @@
+package io.github.sds100.keymapper.common.utils
+
+/**
+ * Represents the physical orientation of the device based on the device's
+ * orientation sensor (accelerometer), independent of the screen rotation setting.
+ */
+enum class PhysicalOrientation {
+ PORTRAIT,
+ LANDSCAPE,
+ PORTRAIT_INVERTED,
+ LANDSCAPE_INVERTED,
+}
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 f13bec696e..a51d8d1e9d 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
@@ -2,14 +2,19 @@ package io.github.sds100.keymapper.data
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
object Keys {
val darkTheme = stringPreferencesKey("pref_dark_theme_mode")
+ /**
+ * libsu was not used in version 3.2.1 and older. The user had to manually enable a setting
+ * for root features to be enabled.
+ */
@Deprecated("Now use the libsu library to detect whether the device is rooted.")
- val hasRootPermission = booleanPreferencesKey("pref_allow_root_features")
+ val hasRootPermissionLegacy = booleanPreferencesKey("pref_allow_root_features")
val shownAppIntro = booleanPreferencesKey("pref_first_time")
@@ -55,9 +60,9 @@ object Keys {
booleanPreferencesKey("key_shown_sequence_trigger_explanation_dialog")
val shownTriggerConstraintsTip =
booleanPreferencesKey("key_shown_trigger_constraints_tip")
- val shownCapsLockProModeTip =
+ val shownCapsLockExpertModeTip =
booleanPreferencesKey("key_shown_caps_lock_pro_mode_compatibility_tip")
- val shownVolumeButtonsProModeTip =
+ val shownVolumeButtonsExpertModeTip =
booleanPreferencesKey("key_shown_volume_buttons_pro_mode_tip")
val shownScreenPinningTip =
booleanPreferencesKey("key_shown_screen_pinning_tip")
@@ -117,27 +122,34 @@ object Keys {
// val skipTapTargetTutorial =
// booleanPreferencesKey("key_skip_tap_target_tutorial")
- val isProModeWarningUnderstood =
+ val isExpertModeWarningUnderstood =
booleanPreferencesKey("key_is_pro_mode_warning_understood")
- val isProModeInteractiveSetupAssistantEnabled =
+ val isExpertModeInteractiveSetupAssistantEnabled =
booleanPreferencesKey("key_is_pro_mode_setup_assistant_enabled")
- val isProModeInfoDismissed =
+ val isExpertModeInfoDismissed =
booleanPreferencesKey("key_is_pro_mode_info_dismissed")
- val isProModeAutoStartBootEnabled =
+ val isSystemBridgeKeepAliveEnabled =
booleanPreferencesKey("key_is_pro_mode_auto_start_boot_enabled")
val isSystemBridgeEmergencyKilled =
booleanPreferencesKey("key_is_system_bridge_emergency_killed")
+ val isSystemBridgeStoppedByUser =
+ booleanPreferencesKey("key_is_system_bridge_stopped_by_user")
+
/**
* Whether the user has started the system bridge before.
*/
val isSystemBridgeUsed = booleanPreferencesKey("key_is_system_bridge_used")
- val isCleanShutdown = booleanPreferencesKey("key_is_clean_shutdown")
+ /**
+ * The last time the system bridge was auto started in time since boot.
+ * Uses SystemClock.elapsedRealtime().
+ */
+ val systemBridgeLastAutoStartTime = longPreferencesKey("key_system_bridge_last_auto_start_time")
val keyEventActionsUseSystemBridge =
booleanPreferencesKey("key_key_event_actions_use_system_bridge")
@@ -145,10 +157,10 @@ object Keys {
val shellCommandScriptText = stringPreferencesKey("key_shell_command_script_text")
/**
- * This is stored as true when PRO Mode has been auto started after upgrading
- * to 4.0 on a rooted device.
+ * This is stored as true when PRO Mode has been auto started after updating
+ * to 4.0 and Key Mapper previously had root or shizuku permission.
*/
- val handledRootToProModeUpgrade = booleanPreferencesKey("key_handled_root_to_pro_mode_upgrade")
+ val handledUpgradeToExpertMode = booleanPreferencesKey("key_handled_upgrade_to_pro_mode")
val handledMigrateScreenOffKeyMapsNotification =
booleanPreferencesKey("key_handled_migrate_screen_off_key_maps_notification")
diff --git a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt
index ae803a96b4..f241647dd6 100644
--- a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt
+++ b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt
@@ -15,11 +15,9 @@ object PreferenceDefaults {
const val SEQUENCE_TRIGGER_TIMEOUT = 1000
const val HOLD_DOWN_DURATION = 1000
- const val PRO_MODE_INTERACTIVE_SETUP_ASSISTANT = true
+ const val EXPERT_MODE_INTERACTIVE_SETUP_ASSISTANT = true
- // Enable this by default so that key maps will still work for root users
- // who upgrade to version 4.0.
- const val PRO_MODE_AUTOSTART_BOOT = true
+ const val EXPERT_MODE_KEEP_ALIVE = true
// It is false by default and the first time they turn on the system bridge,
// the preference will be set to true.
diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt
index 24d5954c60..3deebac611 100644
--- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt
+++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt
@@ -58,6 +58,13 @@ data class ConstraintEntity(
const val ORIENTATION_PORTRAIT = "constraint_orientation_portrait"
const val ORIENTATION_LANDSCAPE = "constraint_orientation_landscape"
+ const val PHYSICAL_ORIENTATION_PORTRAIT = "constraint_physical_orientation_portrait"
+ const val PHYSICAL_ORIENTATION_LANDSCAPE = "constraint_physical_orientation_landscape"
+ const val PHYSICAL_ORIENTATION_PORTRAIT_INVERTED =
+ "constraint_physical_orientation_portrait_inverted"
+ const val PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED =
+ "constraint_physical_orientation_landscape_inverted"
+
const val FLASHLIGHT_ON = "flashlight_on"
const val FLASHLIGHT_OFF = "flashlight_off"
diff --git a/evdev/.gitignore b/evdev/.gitignore
new file mode 100644
index 0000000000..48b4c6e25e
--- /dev/null
+++ b/evdev/.gitignore
@@ -0,0 +1,5 @@
+/build
+.cxx
+/src/main/cpp/libevdev/event-names.h
+/src/main/cpp/aidl/*
+/src/main/rust/**/target/
diff --git a/evdev/.idea/.gitignore b/evdev/.idea/.gitignore
new file mode 100644
index 0000000000..13566b81b0
--- /dev/null
+++ b/evdev/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/evdev/.idea/dictionaries/project.xml b/evdev/.idea/dictionaries/project.xml
new file mode 100644
index 0000000000..77c05f2c6b
--- /dev/null
+++ b/evdev/.idea/dictionaries/project.xml
@@ -0,0 +1,7 @@
+
+
+
+ uinput
+
+
+
\ No newline at end of file
diff --git a/evdev/.idea/gradle.xml b/evdev/.idea/gradle.xml
new file mode 100644
index 0000000000..e77e5a3224
--- /dev/null
+++ b/evdev/.idea/gradle.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/evdev/.idea/inspectionProfiles/Project_Default.xml b/evdev/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000000..c5e1cad975
--- /dev/null
+++ b/evdev/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/evdev/.idea/misc.xml b/evdev/.idea/misc.xml
new file mode 100644
index 0000000000..e8e8dad566
--- /dev/null
+++ b/evdev/.idea/misc.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/evdev/.idea/rust.xml b/evdev/.idea/rust.xml
new file mode 100644
index 0000000000..7bc91eafa3
--- /dev/null
+++ b/evdev/.idea/rust.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/evdev/.idea/vcs.xml b/evdev/.idea/vcs.xml
new file mode 100644
index 0000000000..6c0b863585
--- /dev/null
+++ b/evdev/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/evdev/build.gradle.kts b/evdev/build.gradle.kts
new file mode 100644
index 0000000000..20a27bb7af
--- /dev/null
+++ b/evdev/build.gradle.kts
@@ -0,0 +1,141 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.mozilla.rust.android)
+}
+
+android {
+ namespace = "io.github.sds100.keymapper.evdev"
+ compileSdk = libs.versions.compile.sdk.get().toInt()
+
+ // IMPORTANT! If you change this, also change it in build.rs for evdev crate and any CI workflows. This must
+ // match one of the NDK versions installed in the GitHub actions runner. E.g look here for Ubuntu 24.04
+ // https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md
+ ndkVersion = "27.3.13750724"
+
+ defaultConfig {
+ // System bridge originally only supported Android 10+ because that was the min sdk
+ // for binder-ndk library. Even though that is no longer used, it is extra effort to support
+ // the changes to the internal Android APIs on Android 8 and 9.
+ minSdk = 29
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+
+ buildFeatures {
+ // Disable because a Java implementation of IEvdevCallback is not required in this module
+ aidl = false
+ }
+
+ packaging {
+ jniLibs {
+ // This replaces extractNativeLibs option in the manifest. This is needed so the
+ // libraries are extracted to a location on disk where the system bridge process
+ // can access them. Start in Android 6.0, they are no longer extracted by default.
+ useLegacyPackaging = true
+
+ // This is required on Android 15. Otherwise a java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH error is thrown.
+ keepDebugSymbols.add("**/*.so")
+ }
+ }
+}
+
+// IMPORTANT! Specify path to rustc and cargo in local.properties with the following lines:
+// rust.cargoCommand=/Users/username/.cargo/bin/cargo
+// rust.rustcCommand=/Users/username/.cargo/bin/rustc
+cargo {
+ val isDebug = gradle.startParameter.taskNames.any { it.lowercase().contains("debug") }
+
+ module = "src/main/rust/evdev_manager"
+ libname = "evdev_manager"
+ targets = if (isDebug) {
+ // Only building for one target saves some time when developing
+ listOf("arm64")
+ } else {
+ listOf("arm", "arm64", "x86", "x86_64")
+ }
+
+ // Can not do this with buildType configurations.
+ // See https://github.com/mozilla/rust-android-gradle/issues/38
+ profile = if (isDebug) {
+ "debug"
+ } else {
+ "release"
+ }
+}
+
+dependencies {
+}
+
+// The list of event names needs to be parsed from the input.h file in the NDK.
+// input.h can be found in the Android/sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/linux/input.h
+// folder on macOS.
+val generateLibEvDevEventNames by tasks.registering(Exec::class) {
+ group = "build"
+ description = "Generates event names header from input.h"
+
+ val prebuiltDir = File(android.ndkDirectory, "toolchains/llvm/prebuilt")
+
+ // The "darwin-x86_64" part of the path is different on each operating system but it seems like
+ // the SDK Manager only downloads the NDK specific to the local operating system. So, just
+ // go into the only directory that the "prebuilt" directory contains.
+ val hostDirs = prebuiltDir.listFiles { file -> file.isDirectory }
+ ?: throw GradleException("No prebuilt toolchain directories found in $prebuiltDir")
+
+ if (hostDirs.size != 1) {
+ throw GradleException(
+ "Expected exactly one prebuilt toolchain directory in $prebuiltDir, found ${hostDirs.size}",
+ )
+ }
+ val toolchainDir = hostDirs[0].absolutePath
+
+ val inputHeader = "$toolchainDir/sysroot/usr/include/linux/input.h"
+ val inputEventCodesHeader = "$toolchainDir/sysroot/usr/include/linux/input-event-codes.h"
+ val outputHeader = "$projectDir/src/main/cpp/libevdev/event-names.h"
+ val pythonScript = "$projectDir/src/main/cpp/libevdev/make-event-names.py"
+
+ commandLine("python3", pythonScript, inputHeader, inputEventCodesHeader)
+
+ standardOutput = File(outputHeader).outputStream()
+
+ inputs.file(pythonScript)
+ inputs.file(inputHeader)
+ inputs.file(inputEventCodesHeader)
+ outputs.file(outputHeader)
+}
+
+// Note: NDK AIDL compilation is no longer needed since we're using pure JNI
+// instead of C++ Binder layer. The Kotlin side still uses IEvdevCallback AIDL,
+// but that's handled by Android's standard AIDL processing.
+
+tasks.named("preBuild") {
+ dependsOn(generateLibEvDevEventNames)
+}
+
+// Ensure event names are generated before cargo build runs
+afterEvaluate {
+ tasks.matching { it.name.contains("cargoBuild") }.configureEach {
+ dependsOn(generateLibEvDevEventNames)
+ }
+}
+
+// Must come after all tasks above, otherwise gradle syncing fails.
+//
+// Run cargo build when the files change.
+// See https://github.com/mozilla/rust-android-gradle/issues/166
+tasks.whenTaskAdded {
+ if (name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders") {
+ outputs.upToDateWhen { false }
+
+ dependsOn("cargoBuild")
+ }
+}
diff --git a/evdev/consumer-rules.pro b/evdev/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/evdev/proguard-rules.pro b/evdev/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/evdev/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/sysbridge/src/main/cpp/libevdev/Makefile.am b/evdev/src/main/cpp/libevdev/Makefile.am
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/Makefile.am
rename to evdev/src/main/cpp/libevdev/Makefile.am
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-int.h b/evdev/src/main/cpp/libevdev/libevdev-int.h
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev-int.h
rename to evdev/src/main/cpp/libevdev/libevdev-int.h
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-names.c b/evdev/src/main/cpp/libevdev/libevdev-names.c
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev-names.c
rename to evdev/src/main/cpp/libevdev/libevdev-names.c
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-uinput-int.h b/evdev/src/main/cpp/libevdev/libevdev-uinput-int.h
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev-uinput-int.h
rename to evdev/src/main/cpp/libevdev/libevdev-uinput-int.h
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-uinput.c b/evdev/src/main/cpp/libevdev/libevdev-uinput.c
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev-uinput.c
rename to evdev/src/main/cpp/libevdev/libevdev-uinput.c
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-uinput.h b/evdev/src/main/cpp/libevdev/libevdev-uinput.h
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev-uinput.h
rename to evdev/src/main/cpp/libevdev/libevdev-uinput.h
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-util.h b/evdev/src/main/cpp/libevdev/libevdev-util.h
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev-util.h
rename to evdev/src/main/cpp/libevdev/libevdev-util.h
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev.c b/evdev/src/main/cpp/libevdev/libevdev.c
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev.c
rename to evdev/src/main/cpp/libevdev/libevdev.c
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev.h b/evdev/src/main/cpp/libevdev/libevdev.h
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev.h
rename to evdev/src/main/cpp/libevdev/libevdev.h
diff --git a/sysbridge/src/main/cpp/libevdev/libevdev.sym b/evdev/src/main/cpp/libevdev/libevdev.sym
similarity index 100%
rename from sysbridge/src/main/cpp/libevdev/libevdev.sym
rename to evdev/src/main/cpp/libevdev/libevdev.sym
diff --git a/sysbridge/src/main/cpp/libevdev/make-event-names.py b/evdev/src/main/cpp/libevdev/make-event-names.py
old mode 100755
new mode 100644
similarity index 99%
rename from sysbridge/src/main/cpp/libevdev/make-event-names.py
rename to evdev/src/main/cpp/libevdev/make-event-names.py
index 743b4b58b1..16a984d960
--- a/sysbridge/src/main/cpp/libevdev/make-event-names.py
+++ b/evdev/src/main/cpp/libevdev/make-event-names.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/_env python3
#
# Parses linux/input.h scanning for #define KEY_FOO 134
# Prints C header files or Python files that can be used as
diff --git a/evdev/src/main/rust/evdev/.cargo/config.toml b/evdev/src/main/rust/evdev/.cargo/config.toml
new file mode 100644
index 0000000000..f2084c6b7f
--- /dev/null
+++ b/evdev/src/main/rust/evdev/.cargo/config.toml
@@ -0,0 +1,15 @@
+[target.armv7-linux-androideabi]
+rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
+
+[target.aarch64-linux-android]
+rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
+
+[target.i686-linux-android]
+rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
+
+[target.x86_64-linux-android]
+rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
+
+
+
+
diff --git a/evdev/src/main/rust/evdev/Cargo.lock b/evdev/src/main/rust/evdev/Cargo.lock
new file mode 100644
index 0000000000..e5c3b3271b
--- /dev/null
+++ b/evdev/src/main/rust/evdev/Cargo.lock
@@ -0,0 +1,53 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "cc"
+version = "1.2.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "evdev"
+version = "0.1.0"
+dependencies = [
+ "bitflags",
+ "cc",
+ "libc",
+ "log",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
+[[package]]
+name = "libc"
+version = "1.0.0-alpha.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7222002e5385b4d9327755661e3847c970e8fbf9dea6da8c57f16e8cfbff53a8"
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
diff --git a/evdev/src/main/rust/evdev/Cargo.toml b/evdev/src/main/rust/evdev/Cargo.toml
new file mode 100644
index 0000000000..489211c191
--- /dev/null
+++ b/evdev/src/main/rust/evdev/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "evdev"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "evdev"
+# "cdylib" is necessary to produce a C-style dynamic library that can be loaded by Android's JNI.
+# "rlib" is necessary to allow this crate to be used as a dependency by other Rust crates.
+crate-type = ["cdylib", "rlib"]
+
+[profile.dev]
+debug = true
+strip = false # Don't strip symbols, keeps them in the .so file
+
+[dependencies]
+libc = "1.0.0-alpha.1"
+log = "0.4.28"
+bitflags = "2.10.0"
+
+[build-dependencies]
+cc = "1.2.46"
diff --git a/evdev/src/main/rust/evdev/README.md b/evdev/src/main/rust/evdev/README.md
new file mode 100644
index 0000000000..f96a1eb7e9
--- /dev/null
+++ b/evdev/src/main/rust/evdev/README.md
@@ -0,0 +1 @@
+Most of the code in this crate is taken from https://github.com/ndesh26/evdev-rs.
\ No newline at end of file
diff --git a/evdev/src/main/rust/evdev/build.rs b/evdev/src/main/rust/evdev/build.rs
new file mode 100644
index 0000000000..c4813c4800
--- /dev/null
+++ b/evdev/src/main/rust/evdev/build.rs
@@ -0,0 +1,133 @@
+use std::env;
+use std::path::{Path, PathBuf};
+
+fn main() {
+ let manifest_dir: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+
+ // Map Rust target architecture to Android ABI directory
+ let target = env::var("TARGET").expect("TARGET environment variable not set");
+
+ // This crate only supports Android targets for actual builds.
+ // For cargo check on host systems, we'll skip C/C++ compilation but still
+ // generate bindings to allow type checking.
+ let is_android = target.contains("android");
+
+ if !is_android {
+ eprintln!(
+ "Warning: Building for non-Android target '{}'. Skipping C/C++ compilation.",
+ target
+ );
+ eprintln!("This crate is designed for Android. Use Gradle for actual builds.");
+ // Skip all compilation but succeed to allow cargo check to work
+ return;
+ }
+
+ // Path to C/C++ source files
+ let cpp_dir = manifest_dir.join("../../cpp");
+
+ println!("cargo:rerun-if-changed={}", cpp_dir.to_str().unwrap());
+
+ // Find Android NDK sysroot for bindgen
+ let ndk_sysroot = find_ndk_sysroot(&manifest_dir);
+ let sysroot_include = ndk_sysroot.join("usr/include");
+
+ let libevdev_dir = cpp_dir.join("libevdev");
+
+ // Build C files from libevdev
+ let mut c_builder = cc::Build::new();
+ c_builder
+ .file(libevdev_dir.join("libevdev.c"))
+ .file(libevdev_dir.join("libevdev-names.c"))
+ .file(libevdev_dir.join("libevdev-uinput.c"))
+ .include(&libevdev_dir)
+ .include(sysroot_include.join("linux/input-event-codes.h"))
+ .flag("-Werror=format")
+ .flag("-Wno-unused-parameter")
+ .flag("-fdata-sections")
+ .flag("-ffunction-sections");
+
+ if env::var("PROFILE").unwrap() == "release" {
+ c_builder.flag("-O2").flag("-fvisibility=hidden");
+ }
+
+ c_builder.compile("evdev_c");
+}
+
+fn find_ndk_sysroot(manifest_dir: &Path) -> PathBuf {
+ let sdk_dir = get_sdk_dir(manifest_dir).expect("SDK directory not available");
+ let ndk_version = "27.3.13750724";
+
+ get_sysroot_for_version(&sdk_dir, ndk_version)
+}
+
+fn get_sdk_dir(manifest_dir: &Path) -> Option {
+ // 1. Read from local.properties file
+ // Navigate from evdev crate to project root
+ let local_properties = manifest_dir.join("../../../../../local.properties");
+
+ if let Ok(contents) = std::fs::read_to_string(&local_properties) {
+ for line in contents.lines() {
+ // Skip comments and empty lines
+ let line = line.trim();
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+
+ // Look for sdk.dir=value or android.sdk.dir=value
+ if let Some(stripped) = line.strip_prefix("sdk.dir=") {
+ let sdk_path = stripped.trim();
+ if !sdk_path.is_empty() {
+ return Some(sdk_path.to_string());
+ }
+ }
+ if let Some(stripped) = line.strip_prefix("android.sdk.dir=") {
+ let sdk_path = stripped.trim();
+ if !sdk_path.is_empty() {
+ return Some(sdk_path.to_string());
+ }
+ }
+ }
+ }
+
+ // 2. Check environment variables
+ if let Ok(sdk_dir) = env::var("ANDROID_SDK_ROOT") {
+ return Some(sdk_dir);
+ }
+
+ if let Ok(sdk_dir) = env::var("ANDROID_HOME") {
+ return Some(sdk_dir);
+ }
+
+ None
+}
+
+fn get_sysroot_for_version(sdk_dir: &str, version: &str) -> PathBuf {
+ // Detect host platform
+ let host = if cfg!(target_os = "macos") {
+ "darwin-x86_64"
+ } else if cfg!(target_os = "linux") {
+ "linux-x86_64"
+ } else if cfg!(target_os = "windows") {
+ "windows-x86_64"
+ } else {
+ panic!("Unsupported host platform for NDK")
+ };
+
+ let sysroot = PathBuf::from(sdk_dir)
+ .join("ndk")
+ .join(version)
+ .join("toolchains")
+ .join("llvm")
+ .join("prebuilt")
+ .join(host)
+ .join("sysroot");
+
+ if !sysroot.exists() {
+ panic!(
+ "Could not find Android NDK sysroot for version {} at {}. Please ensure NDK {} is installed in {}/ndk/",
+ version, sysroot.display(), version, sdk_dir
+ );
+ }
+
+ sysroot
+}
diff --git a/evdev/src/main/rust/evdev/src/device.rs b/evdev/src/main/rust/evdev/src/device.rs
new file mode 100644
index 0000000000..da1d135206
--- /dev/null
+++ b/evdev/src/main/rust/evdev/src/device.rs
@@ -0,0 +1,900 @@
+use crate::{AbsInfo, GrabMode, InputEvent, LedState, ReadFlag, ReadStatus, TimeVal};
+use libc::{c_int, c_uint, c_void};
+use libc::{fcntl, F_GETFD};
+use std::ffi::CString;
+use std::fs::File;
+use std::fs::OpenOptions;
+use std::io::Read;
+use std::mem::ManuallyDrop;
+use std::os::fd::AsFd;
+use std::os::unix::fs::OpenOptionsExt;
+use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
+use std::path::Path;
+use std::{io, ptr};
+
+use crate::enums::*;
+use crate::libevdev;
+use crate::util::*;
+
+/// Types that can be enabled on a DeviceWrapper (i.e. buttons, keys, relative motion)
+///
+/// Generally this method will not be called directly, but will insted be called through [Device::enable()](crate::Device::enable)
+///
+/// ```rust
+/// # use evdev_rs::{UninitDevice, DeviceWrapper, Enable, enums::{EventCode, EV_REL::REL_X}};
+/// let dev = UninitDevice::new().expect("Device creation failed");
+/// dev.enable(EventCode::EV_REL(REL_X)).expect("Enable failed");
+/// ```
+///
+/// If you need to enable a EV_ABS or EV_REP event code, use
+/// [enable_event_code](crate::Device::enable_event_code), as this
+/// implementation doesn't pass EV_ABS data.
+pub trait Enable {
+ fn enable(&self, device: &D) -> io::Result<()>;
+ fn disable(&self, device: &D) -> io::Result<()>;
+ fn has(&self, device: &D) -> bool;
+}
+
+impl Enable for InputProp {
+ fn enable(&self, device: &D) -> io::Result<()> {
+ device.enable_property(self)
+ }
+ fn disable(&self, device: &D) -> io::Result<()> {
+ device.disable_property(self)
+ }
+ fn has(&self, device: &D) -> bool {
+ device.has_property(self)
+ }
+}
+
+impl Enable for EventType {
+ fn enable(&self, device: &D) -> io::Result<()> {
+ device.enable_event_type(self)
+ }
+ fn disable(&self, device: &D) -> io::Result<()> {
+ device.disable_event_type(self)
+ }
+ fn has(&self, device: &D) -> bool {
+ device.has_event_type(self)
+ }
+}
+
+impl Enable for EventCode {
+ fn enable(&self, device: &D) -> io::Result<()> {
+ device.enable_event_code(self, None)
+ }
+ fn disable(&self, device: &D) -> io::Result<()> {
+ device.disable_event_code(self)
+ }
+ fn has(&self, device: &D) -> bool {
+ device.has_event_code(self)
+ }
+}
+
+/// Extra data for use with enable_event_code
+#[derive(Clone, Copy, Debug)]
+pub enum EnableCodeData {
+ AbsInfo(AbsInfo),
+ RepInfo(i32),
+}
+
+/// Abstraction over structs which contain an inner `*mut libevdev`
+pub trait DeviceWrapper: Sized {
+ fn raw(&self) -> *mut libevdev::libevdev;
+
+ /// Forcibly enable an EventType/InputProp on this device, even if the underlying
+ /// device does not support it. While this cannot make the device actually
+ /// report such events, it will now return true for has().
+ ///
+ /// This is a local modification only affecting only this representation of
+ /// this device.
+ fn enable(&self, e: E) -> io::Result<()> {
+ e.enable(self)
+ }
+
+ /// Enables this property, a call to `set_file` will overwrite any previously set values
+ ///
+ /// Note: Please use the `enable` function instead. This function is only
+ /// available for the sake of maintaining compatibility with libevdev.
+ fn enable_property(&self, prop: &InputProp) -> io::Result<()> {
+ let result =
+ unsafe { libevdev::libevdev_enable_property(self.raw(), *prop as c_uint) as i32 };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ /// Forcibly enable an event type on this device, even if the underlying
+ /// device does not support it. While this cannot make the device actually
+ /// report such events, it will now return true for libevdev_has_event_type().
+ ///
+ /// This is a local modification only affecting only this representation of
+ /// this device.
+ ///
+ /// Note: Please use the `enable` function instead. This function is only
+ /// available for the sake of maintaining compatibility with libevdev.
+ fn enable_event_type(&self, ev_type: &EventType) -> io::Result<()> {
+ let result =
+ unsafe { libevdev::libevdev_enable_event_type(self.raw(), *ev_type as c_uint) };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ /// Forcibly enable an event type on this device, even if the underlying
+ /// device does not support it. While this cannot make the device actually
+ /// report such events, it will now return true for libevdev_has_event_code().
+ ///
+ /// The last argument depends on the type and code:
+ /// If type is EV_ABS, data must be a pointer to a struct input_absinfo
+ /// containing the data for this axis.
+ /// If type is EV_REP, data must be a pointer to a int containing the data
+ /// for this axis.
+ /// For all other types, the argument must be `None`.
+ ///
+ /// Note: Please use the `enable` function instead. This function is only
+ /// available for the sake of maintaining compatibility with libevdev.
+ fn enable_event_code(
+ &self,
+ ev_code: &EventCode,
+ data: Option,
+ ) -> io::Result<()> {
+ let data = match ev_code {
+ EventCode::EV_ABS(_) => match data {
+ Some(EnableCodeData::AbsInfo(info)) => &info.as_raw() as *const _ as *const c_void,
+ _ => {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "EventCode::EV_ABS must be paired with EnableCodeData::AbsInfo",
+ ))
+ }
+ },
+ EventCode::EV_REP(_) => match data {
+ Some(EnableCodeData::RepInfo(info)) => {
+ &libc::c_int::from(info) as *const _ as *const c_void
+ }
+ _ => {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "EventCode::EV_REP must be paired with EnableCodeData::RepInfo",
+ ))
+ }
+ },
+ _ => ptr::null(),
+ };
+
+ let (ev_type, ev_code) = event_code_to_int(ev_code);
+
+ let result =
+ unsafe { libevdev::libevdev_enable_event_code(self.raw(), ev_type, ev_code, data) };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ /// Forcibly disable an EventType/EventCode on this device, even if the
+ /// underlying device provides it. This effectively mutes the respective set of
+ /// events. has() will return false for this EventType/EventCode
+ ///
+ /// In most cases, a caller likely only wants to disable a single code, not
+ /// the whole type.
+ ///
+ /// Disabling EV_SYN will not work. In Peter's Words "Don't shoot yourself
+ /// in the foot. It hurts".
+ ///
+ /// This is a local modification only affecting only this representation of
+ /// this device.
+ fn disable(&self, d: E) -> io::Result<()> {
+ d.disable(self)
+ }
+
+ /// Forcibly disable an event type on this device, even if the underlying
+ /// device provides it. This effectively mutes the respective set of
+ /// events. libevdev will filter any events matching this type and none will
+ /// reach the caller. libevdev_has_event_type() will return false for this
+ /// type.
+ ///
+ /// In most cases, a caller likely only wants to disable a single code, not
+ /// the whole type. Use `disable_event_code` for that.
+ ///
+ /// Disabling EV_SYN will not work. In Peter's Words "Don't shoot yourself
+ /// in the foot. It hurts".
+ ///
+ /// This is a local modification only affecting only this representation of
+ /// this device.
+ ///
+ /// Note: Please use the `disable` function instead. This function is only
+ /// available for the sake of maintaining compatibility with libevdev.
+ fn disable_event_type(&self, ev_type: &EventType) -> io::Result<()> {
+ let result =
+ unsafe { libevdev::libevdev_disable_event_type(self.raw(), *ev_type as c_uint) };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+ /// Forcibly disable an event code on this device, even if the underlying
+ /// device provides it. This effectively mutes the respective set of
+ /// events. libevdev will filter any events matching this type and code and
+ /// none will reach the caller. `has_event_code` will return false for
+ /// this code.
+ ///
+ /// Disabling all event codes for a given type will not disable the event
+ /// type. Use `disable_event_type` for that.
+ ///
+ /// This is a local modification only affecting only this representation of
+ /// this device.
+ ///
+ /// Disabling codes of type EV_SYN will not work. Don't shoot yourself in the
+ /// foot. It hurts.
+ ///
+ /// Note: Please use the `disable` function instead. This function is only
+ /// available for the sake of maintaining compatibility with libevdev.
+ fn disable_event_code(&self, code: &EventCode) -> io::Result<()> {
+ let (ev_type, ev_code) = event_code_to_int(code);
+ let result = unsafe { libevdev::libevdev_disable_event_code(self.raw(), ev_type, ev_code) };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ fn disable_property(&self, prop: &InputProp) -> io::Result<()> {
+ let result = unsafe { libevdev::libevdev_disable_property(self.raw(), (*prop) as c_uint) };
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ /// Returns `true` if device support the InputProp/EventType/EventCode and false otherwise
+ fn has(&self, e: E) -> bool {
+ e.has(self)
+ }
+
+ /// Returns `true` if device support the property and false otherwise
+ ///
+ /// Note: Please use the `has` function instead. This function is only
+ /// available for the sake of maintaining compatibility with libevdev.
+ fn has_property(&self, prop: &InputProp) -> bool {
+ unsafe { libevdev::libevdev_has_property(self.raw(), *prop as c_uint) != 0 }
+ }
+
+ /// Returns `true` is the device support this event type and `false` otherwise
+ ///
+ /// Note: Please use the `has` function instead. This function is only
+ /// available for the sake of maintaining compatibility with libevdev.
+ fn has_event_type(&self, ev_type: &EventType) -> bool {
+ unsafe { libevdev::libevdev_has_event_type(self.raw(), *ev_type as c_uint) != 0 }
+ }
+
+ /// Return `true` is the device support this event type and code and `false` otherwise
+ ///
+ /// Note: Please use the `has` function instead. This function is only
+ /// available for the sake of maintaining compatibility with libevdev.
+ fn has_event_code(&self, code: &EventCode) -> bool {
+ unsafe {
+ let (ev_type, ev_code) = event_code_to_int(code);
+ libevdev::libevdev_has_event_code(self.raw(), ev_type, ev_code) != 0
+ }
+ }
+
+ string_getter!(
+ #[doc = "Get device's name, as set by the kernel, or overridden by a call to `set_name`"],
+ name, libevdev_get_name,
+ #[doc = "Get device's physical location, as set by the kernel, or overridden by a call to `set_phys`"],
+ phys, libevdev_get_phys,
+ #[doc = "Get device's unique identifier, as set by the kernel, or overridden by a call to `set_uniq`"],
+ uniq, libevdev_get_uniq
+ );
+
+ string_setter!(
+ set_name,
+ libevdev_set_name,
+ set_phys,
+ libevdev_set_phys,
+ set_uniq,
+ libevdev_set_uniq
+ );
+
+ product_getter!(
+ product_id,
+ libevdev_get_id_product,
+ vendor_id,
+ libevdev_get_id_vendor,
+ bustype,
+ libevdev_get_id_bustype,
+ version,
+ libevdev_get_id_version
+ );
+
+ product_setter!(
+ set_product_id,
+ libevdev_set_id_product,
+ set_vendor_id,
+ libevdev_set_id_vendor,
+ set_bustype,
+ libevdev_set_id_bustype,
+ set_version,
+ libevdev_set_id_version
+ );
+
+ /// Get the axis info for the given axis, as advertised by the kernel.
+ ///
+ /// Returns the `AbsInfo` for the given the code or None if the device
+ /// doesn't support this code
+ fn abs_info(&self, code: &EventCode) -> Option {
+ let (_, ev_code) = event_code_to_int(code);
+ let a = unsafe { libevdev::libevdev_get_abs_info(self.raw(), ev_code).as_ref()? };
+
+ Some(AbsInfo {
+ value: a.value,
+ minimum: a.minimum,
+ maximum: a.maximum,
+ fuzz: a.fuzz,
+ flat: a.flat,
+ resolution: a.resolution,
+ })
+ }
+
+ /// Change the abs info for the given EV_ABS event code, if the code exists.
+ ///
+ /// This function has no effect if `has_event_code` returns false for
+ /// this code.
+ fn set_abs_info(&self, code: &EventCode, absinfo: &AbsInfo) {
+ let (_, ev_code) = event_code_to_int(code);
+
+ unsafe {
+ libevdev::libevdev_set_abs_info(self.raw(), ev_code, &absinfo.as_raw() as *const _);
+ }
+ }
+
+ /// Returns the current value of the event type.
+ ///
+ /// If the device supports this event type and code, the return value is
+ /// set to the current value of this axis. Otherwise, `None` is returned.
+ fn event_value(&self, code: &EventCode) -> Option {
+ let mut value: i32 = 0;
+ let (ev_type, ev_code) = event_code_to_int(code);
+ let valid = unsafe {
+ libevdev::libevdev_fetch_event_value(self.raw(), ev_type, ev_code, &mut value)
+ };
+
+ match valid {
+ 0 => None,
+ _ => Some(value),
+ }
+ }
+
+ /// Set the value for a given event type and code.
+ ///
+ /// This only makes sense for some event types, e.g. setting the value for
+ /// EV_REL is pointless.
+ ///
+ /// This is a local modification only affecting only this representation of
+ /// this device. A future call to event_value() will return this
+ /// value, unless the value was overwritten by an event.
+ ///
+ /// If the device supports ABS_MT_SLOT, the value set for any ABS_MT_*
+ /// event code is the value of the currently active slot. You should use
+ /// `set_slot_value` instead.
+ ///
+ /// If the device supports ABS_MT_SLOT and the type is EV_ABS and the code is
+ /// ABS_MT_SLOT, the value must be a positive number less then the number of
+ /// slots on the device. Otherwise, `set_event_value` returns Err.
+ fn set_event_value(&self, code: &EventCode, val: i32) -> io::Result<()> {
+ let (ev_type, ev_code) = event_code_to_int(code);
+ let result = unsafe {
+ libevdev::libevdev_set_event_value(self.raw(), ev_type, ev_code, val as c_int)
+ };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ abs_getter!(
+ abs_minimum,
+ libevdev_get_abs_minimum,
+ abs_maximum,
+ libevdev_get_abs_maximum,
+ abs_fuzz,
+ libevdev_get_abs_fuzz,
+ abs_flat,
+ libevdev_get_abs_flat,
+ abs_resolution,
+ libevdev_get_abs_resolution
+ );
+
+ abs_setter!(
+ set_abs_minimum,
+ libevdev_set_abs_minimum,
+ set_abs_maximum,
+ libevdev_set_abs_maximum,
+ set_abs_fuzz,
+ libevdev_set_abs_fuzz,
+ set_abs_flat,
+ libevdev_set_abs_flat,
+ set_abs_resolution,
+ libevdev_set_abs_resolution
+ );
+
+ /// Return the current value of the code for the given slot.
+ ///
+ /// If the device supports this event code, the return value is
+ /// is set to the current value of this axis. Otherwise, or
+ /// if the event code is not an ABS_MT_* event code, `None` is returned
+ fn slot_value(&self, slot: u32, code: &EventCode) -> Option {
+ let (_, ev_code) = event_code_to_int(code);
+ let mut value: i32 = 0;
+ let valid = unsafe {
+ libevdev::libevdev_fetch_slot_value(self.raw(), slot as c_uint, ev_code, &mut value)
+ };
+
+ match valid {
+ 0 => None,
+ _ => Some(value),
+ }
+ }
+
+ /// Set the value for a given code for the given slot.
+ ///
+ /// This is a local modification only affecting only this representation of
+ /// this device. A future call to `slot_value` will return this value,
+ /// unless the value was overwritten by an event.
+ ///
+ /// This function does not set event values for axes outside the ABS_MT range,
+ /// use `set_event_value` instead.
+ fn set_slot_value(&self, slot: u32, code: &EventCode, val: i32) -> io::Result<()> {
+ let (_, ev_code) = event_code_to_int(code);
+ let result = unsafe {
+ libevdev::libevdev_set_slot_value(self.raw(), slot as c_uint, ev_code, val as c_int)
+ };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ /// Get the number of slots supported by this device.
+ ///
+ /// The number of slots supported, or `None` if the device does not provide
+ /// any slots
+ ///
+ /// A device may provide ABS_MT_SLOT but a total number of 0 slots. Hence
+ /// the return value of `None` for "device does not provide slots at all"
+ fn num_slots(&self) -> Option {
+ let result = unsafe { libevdev::libevdev_get_num_slots(self.raw()) };
+
+ match result {
+ -1 => None,
+ slots => Some(slots),
+ }
+ }
+
+ /// Get the currently active slot.
+ ///
+ /// This may differ from the value an ioctl may return at this time as
+ /// events may have been read off the file since changing the slot value
+ /// but those events are still in the buffer waiting to be processed.
+ /// The returned value is the value a caller would see if it were to
+ /// process events manually one-by-one.
+ fn current_slot(&self) -> Option {
+ let result = unsafe { libevdev::libevdev_get_current_slot(self.raw()) };
+
+ match result {
+ -1 => None,
+ slots => Some(slots),
+ }
+ }
+}
+
+/// Opaque struct representing an evdev device with no backing file
+pub struct UninitDevice {
+ raw: *mut libevdev::libevdev,
+}
+
+unsafe impl Send for UninitDevice {}
+
+impl DeviceWrapper for UninitDevice {
+ fn raw(&self) -> *mut libevdev::libevdev {
+ self.raw
+ }
+}
+
+impl UninitDevice {
+ /// Initialize a new libevdev device.
+ ///
+ /// Generally you should use Device::new_from_file instead of this method
+ /// This function only initializes the struct to sane default values.
+ /// To actually hook up the device to a kernel device, use `set_file`.
+ pub fn new() -> Option {
+ let libevdev = unsafe { libevdev::libevdev_new() };
+
+ if libevdev.is_null() {
+ None
+ } else {
+ Some(UninitDevice { raw: libevdev })
+ }
+ }
+
+ /// Set the file for this struct and initialize internal data.
+ ///
+ /// If the device changed and you need to re-read a device, use `Device::new_from_file` method.
+ /// If you need to change the file after
+ /// closing and re-opening the same device, use `change_file`.
+ pub fn set_file(self, file: File) -> io::Result {
+ // Don't call UninitDevice's destructor so we can reuse the inner libevdev
+ let leak = ManuallyDrop::new(self);
+ let result = unsafe { libevdev::libevdev_set_fd(leak.raw, file.as_raw_fd()) };
+ match result {
+ 0 => Ok(Device {
+ file,
+ raw: leak.raw,
+ }),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ #[deprecated(
+ since = "0.5.0",
+ note = "Prefer `set_file`. Some function names were changed so they
+ more closely match their type signature. See issue 42 for discussion
+ https://github.com/ndesh26/evdev-rs/issues/42"
+ )]
+ pub fn set_fd(self, file: File) -> io::Result {
+ self.set_file(file)
+ }
+}
+
+impl Drop for UninitDevice {
+ fn drop(&mut self) {
+ unsafe {
+ libevdev::libevdev_free(self.raw);
+ }
+ }
+}
+
+impl std::fmt::Debug for UninitDevice {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.debug_struct("UninitDevice")
+ .field("name", &self.name())
+ .field("phys", &self.phys())
+ .field("uniq", &self.uniq())
+ .finish()
+ }
+}
+
+/// Opaque struct representing an evdev device
+///
+/// Unlike libevdev, this `Device` maintains an associated file as an invariant
+pub struct Device {
+ file: File,
+ raw: *mut libevdev::libevdev,
+}
+
+unsafe impl Send for Device {}
+
+impl DeviceWrapper for Device {
+ fn raw(&self) -> *mut libevdev::libevdev {
+ self.raw
+ }
+}
+
+impl Device {
+ /// Initialize a new libevdev device from the given file.
+ ///
+ /// This is a shortcut for
+ ///
+ /// ```rust,no_run
+ /// use evdev_rs::{Device, UninitDevice};
+ /// # use std::fs::File;
+ ///
+ /// let uninit_device = UninitDevice::new().unwrap();
+ /// # let file = File::open("/dev/input/event0").unwrap();
+ /// let device = uninit_device.set_file(file);
+ /// ```
+ ///
+ /// The caller is responsible for opening the file and setting
+ /// the `O_NONBLOCK` flag and handling permissions.
+ /// If the file is opened without O_NONBLOCK flag then next_event
+ /// should be called with ReadFlag::BLOCKING. Due to the caching
+ /// nature of next_event we might block while trying to buffer
+ /// new events even though some events are already present.
+ pub fn new_from_file(file: File) -> io::Result {
+ let mut libevdev = std::ptr::null_mut();
+ let result = unsafe { libevdev::libevdev_new_from_fd(file.as_raw_fd(), &mut libevdev) };
+
+ match result {
+ 0 => Ok(Device {
+ file,
+ raw: libevdev,
+ }),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ #[deprecated(
+ since = "0.5.0",
+ note = "Prefer `new_from_file`. Some function names were changed so they
+ more closely match their type signature. See issue 42 for discussion
+ https://github.com/ndesh26/evdev-rs/issues/42"
+ )]
+ pub fn new_from_fd(file: File) -> io::Result {
+ Self::new_from_file(file)
+ }
+
+ /// Opens a device with the given path as the location of devnode
+ ///
+ /// The devnode file is opened with `O_NONBLOCK` and all the pending
+ /// events are first read from the file before creating the device.
+ pub fn new_from_path>(path: P) -> io::Result {
+ let mut file = OpenOptions::new()
+ .read(true)
+ .write(false) // Writing is only allowed when rooted so must be false
+ .custom_flags(libc::O_NONBLOCK)
+ .open(path)?;
+ let mut buffer = [0u8; 20 * std::mem::size_of::()];
+
+ let last_result = loop {
+ let result = file.read(&mut buffer);
+ if result.is_err() {
+ break result;
+ }
+ };
+ let _error_code = io::Error::from(io::ErrorKind::WouldBlock);
+ match last_result {
+ Err(_error_code) => Self::new_from_file(file),
+ _ => Err(io::Error::new(
+ io::ErrorKind::WouldBlock,
+ "Unable to open file with O_NONBLOCK",
+ )),
+ }
+ }
+
+ /// Returns the file associated with the device
+ pub fn file(&self) -> &File {
+ &self.file
+ }
+
+ #[deprecated(
+ since = "0.5.0",
+ note = "Prefer `file`. This function can easily be misused. Calling
+ this method, then dropping the returned file will lead to failures
+ e.g. next_event will return an Err()"
+ )]
+ pub fn fd(&self) -> Option {
+ let result = unsafe { libevdev::libevdev_get_fd(self.raw) };
+ match result {
+ 0 => None,
+ _ => unsafe { Some(File::from_raw_fd(result)) },
+ }
+ }
+
+ /// Change the file for this device, without re-reading the actual device.
+ ///
+ /// On success, returns the file that was previously associated with this
+ /// device.
+ ///
+ /// If the file changes after initializing the device, for example after a
+ /// VT-switch in the X.org X server, this function updates the internal
+ /// file to the newly opened. No check is made that new file points to the
+ /// same device. If the device has changed, evdev's behavior is undefined.
+ ///
+ /// evdev device does not sync itself after changing the file and keeps the
+ /// current device state. Use next_event with the FORCE_SYNC flag to force
+ /// a re-sync.
+ ///
+ /// # Example
+ ///
+ /// ```rust,no_run
+ /// use evdev_rs::{Device, UninitDevice, ReadFlag, ReadStatus};
+ /// # use std::fs::File;
+ /// # fn hidden() -> std::io::Result<()> {
+ /// let mut dev = Device::new_from_file(File::open("/dev/input/input0")?)?;
+ /// dev.change_file(File::open("/dev/input/input1")?)?;
+ /// dev.next_event(ReadFlag::FORCE_SYNC);
+ /// while dev.next_event(ReadFlag::SYNC).ok().unwrap().0 == ReadStatus::Sync
+ /// {} // noop
+ /// # Ok(())
+ /// # }
+ /// ```
+ /// After changing the file, the device is assumed ungrabbed and a caller must
+ /// call libevdev_grab() again.
+ pub fn change_file(&mut self, file: File) -> io::Result {
+ let result = unsafe { libevdev::libevdev_change_fd(self.raw, file.as_raw_fd()) };
+
+ match result {
+ 0 => {
+ let mut file = file;
+ std::mem::swap(&mut file, &mut self.file);
+ Ok(file)
+ }
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ #[deprecated(
+ since = "0.5.0",
+ note = "Prefer new_from_file. Some function names were changed so they
+ more closely match their type signature. See issue 42 for discussion
+ https://github.com/ndesh26/evdev-rs/issues/42"
+ )]
+ pub fn change_fd(&mut self, file: File) -> io::Result<()> {
+ self.change_file(file)?;
+ Ok(())
+ }
+
+ /// Grab or ungrab the device through a kernel EVIOCGRAB.
+ ///
+ /// This prevents other clients (including kernel-internal ones such as
+ /// rfkill) from receiving events from this device. This is generally a
+ /// bad idea. Don't do this. Grabbing an already grabbed device, or
+ /// ungrabbing an ungrabbed device is a noop and always succeeds.
+ ///
+ /// A grab is an operation tied to a file descriptor, not a device. If a
+ /// client changes the file descriptor with Device::change_file(), it must
+ /// also re-issue a grab with libevdev_grab().
+ pub fn grab(&mut self, grab: GrabMode) -> io::Result<()> {
+ let result = unsafe { libevdev::libevdev_grab(self.raw, grab as c_int) };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ /// Check if there are events waiting for us.
+ ///
+ /// This function does not consume an event and may not access the device
+ /// file at all. If there are events queued internally this function will
+ /// return true. If the internal queue is empty, this function will poll
+ /// the file descriptor for data.
+ ///
+ /// This is a convenience function for simple processes, most complex programs
+ /// are expected to use select(2) or poll(2) on the file descriptor. The kernel
+ /// guarantees that if data is available, it is a multiple of sizeof(struct
+ /// input_event), and thus calling `next_event` when select(2) or
+ /// poll(2) return is safe. You do not need `has_event_pending` if
+ /// you're using select(2) or poll(2).
+ pub fn has_event_pending(&self) -> bool {
+ unsafe { libevdev::libevdev_has_event_pending(self.raw) > 0 }
+ }
+
+ /// Return the driver version of a device already intialize with `set_file`
+ pub fn driver_version(&self) -> i32 {
+ unsafe { libevdev::libevdev_get_driver_version(self.raw) as i32 }
+ }
+
+ /// Set the device's EV_ABS axis to the value defined in the abs
+ /// parameter. This will be written to the kernel.
+ pub fn set_kernel_abs_info(&self, code: &EventCode, absinfo: &AbsInfo) {
+ let (_, ev_code) = event_code_to_int(code);
+
+ unsafe {
+ libevdev::libevdev_kernel_set_abs_info(
+ self.raw,
+ ev_code,
+ &absinfo.as_raw() as *const _,
+ );
+ }
+ }
+
+ /// Turn an LED on or off.
+ ///
+ /// enabling an LED requires write permissions on the device's file descriptor.
+ pub fn kernel_set_led_value(&self, code: &EventCode, value: LedState) -> io::Result<()> {
+ let (_, ev_code) = event_code_to_int(code);
+ let result =
+ unsafe { libevdev::libevdev_kernel_set_led_value(self.raw, ev_code, value as c_int) };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ /// Set the clock ID to be used for timestamps. Further events from this device
+ /// will report an event time based on the given clock.
+ ///
+ /// This is a modification only affecting this representation of
+ /// this device.
+ pub fn set_clock_id(&self, clockid: i32) -> io::Result<()> {
+ let result = unsafe { libevdev::libevdev_set_clock_id(self.raw, clockid) };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ /// Get the next event from the device. This function operates in two different
+ /// modes: normal mode or sync mode.
+ ///
+ /// In normal mode (when flags has `evdev::NORMAL` set), this function returns
+ /// `ReadStatus::Success` and returns the event. If no events are available at
+ /// this time, it returns `-EAGAIN` as `Err`.
+ ///
+ /// If the current event is an `EV_SYN::SYN_DROPPED` event, this function returns
+ /// `ReadStatus::Sync` and is set to the `EV_SYN` event.The caller should now call
+ /// this function with the `evdev::SYNC` flag set, to get the set of events that
+ /// make up the device state delta. This function returns ReadStatus::Sync for
+ /// each event part of that delta, until it returns `-EAGAIN` once all events
+ /// have been synced.
+ ///
+ /// If a device needs to be synced by the caller but the caller does not call
+ /// with the `evdev::SYNC` flag set, all events from the diff are dropped after
+ /// evdev updates its internal state and event processing continues as normal.
+ /// Note that the current slot and the state of touch points may have updated
+ /// during the `SYN_DROPPED` event, it is strongly recommended that a caller
+ /// ignoring all sync events calls `current_slot` and checks the
+ /// `ABS_MT_TRACKING_ID` values for all slots.
+ ///
+ /// If a device has changed state without events being enqueued in evdev,
+ /// e.g. after changing the file descriptor, use the `evdev::FORCE_SYNC` flag.
+ /// This triggers an internal sync of the device and `next_event` returns
+ /// `ReadStatus::Sync`.
+ pub fn next_event(&self, flags: ReadFlag) -> io::Result<(ReadStatus, InputEvent)> {
+ let mut ev = libevdev::input_event {
+ time: libevdev::timeval {
+ tv_sec: 0,
+ tv_usec: 0,
+ },
+ type_: 0,
+ code: 0,
+ value: 0,
+ };
+
+ let result =
+ unsafe { libevdev::libevdev_next_event(self.raw, flags.bits() as c_uint, &mut ev) };
+
+ let event = InputEvent {
+ time: TimeVal {
+ tv_sec: ev.time.tv_sec,
+ tv_usec: ev.time.tv_usec,
+ },
+ event_code: int_to_event_code(ev.type_ as u32, ev.code as u32),
+ value: ev.value,
+ };
+
+ match result {
+ libevdev::LIBEVDEV_READ_STATUS_SUCCESS => Ok((ReadStatus::Success, event)),
+ libevdev::LIBEVDEV_READ_STATUS_SYNC => Ok((ReadStatus::Sync, event)),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+}
+
+impl Drop for Device {
+ fn drop(&mut self) {
+ unsafe {
+ libevdev::libevdev_free(self.raw);
+ }
+ }
+}
+
+impl std::fmt::Debug for Device {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.debug_struct("Device")
+ .field("name", &self.name())
+ .field("phys", &self.phys())
+ .field("uniq", &self.uniq())
+ .finish()
+ }
+}
+
+impl AsRawFd for Device {
+ fn as_raw_fd(&self) -> RawFd {
+ self.file.as_raw_fd()
+ }
+}
diff --git a/evdev/src/main/rust/evdev/src/enums.rs b/evdev/src/main/rust/evdev/src/enums.rs
new file mode 100644
index 0000000000..02831b9335
--- /dev/null
+++ b/evdev/src/main/rust/evdev/src/enums.rs
@@ -0,0 +1,2713 @@
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EventType {
+ EV_SYN = 0,
+ EV_KEY = 1,
+ EV_REL = 2,
+ EV_ABS = 3,
+ EV_MSC = 4,
+ EV_SW = 5,
+ EV_LED = 17,
+ EV_SND = 18,
+ EV_REP = 20,
+ EV_FF = 21,
+ EV_PWR = 22,
+ EV_FF_STATUS = 23,
+ EV_UNK,
+ EV_MAX = 31,
+}
+
+pub const fn int_to_event_type(code: u32) -> Option {
+ match code {
+ 0 => Some(EventType::EV_SYN),
+ 1 => Some(EventType::EV_KEY),
+ 2 => Some(EventType::EV_REL),
+ 3 => Some(EventType::EV_ABS),
+ 4 => Some(EventType::EV_MSC),
+ 5 => Some(EventType::EV_SW),
+ 17 => Some(EventType::EV_LED),
+ 18 => Some(EventType::EV_SND),
+ 20 => Some(EventType::EV_REP),
+ 21 => Some(EventType::EV_FF),
+ 22 => Some(EventType::EV_PWR),
+ 23 => Some(EventType::EV_FF_STATUS),
+ c if c < 31 => Some(EventType::EV_UNK),
+ 31 => Some(EventType::EV_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EventType {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "EV_SYN" => Ok(EventType::EV_SYN),
+ "EV_KEY" => Ok(EventType::EV_KEY),
+ "EV_REL" => Ok(EventType::EV_REL),
+ "EV_ABS" => Ok(EventType::EV_ABS),
+ "EV_MSC" => Ok(EventType::EV_MSC),
+ "EV_SW" => Ok(EventType::EV_SW),
+ "EV_LED" => Ok(EventType::EV_LED),
+ "EV_SND" => Ok(EventType::EV_SND),
+ "EV_REP" => Ok(EventType::EV_REP),
+ "EV_FF" => Ok(EventType::EV_FF),
+ "EV_PWR" => Ok(EventType::EV_PWR),
+ "EV_FF_STATUS" => Ok(EventType::EV_FF_STATUS),
+ "EV_MAX" => Ok(EventType::EV_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub enum EventCode {
+ EV_SYN(EV_SYN),
+ EV_KEY(EV_KEY),
+ EV_REL(EV_REL),
+ EV_ABS(EV_ABS),
+ EV_MSC(EV_MSC),
+ EV_SW(EV_SW),
+ EV_LED(EV_LED),
+ EV_SND(EV_SND),
+ EV_REP(EV_REP),
+ EV_FF(EV_FF),
+ EV_PWR,
+ EV_FF_STATUS(EV_FF),
+ EV_UNK { event_type: u32, event_code: u32 },
+ EV_MAX,
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_REL {
+ REL_X = 0,
+ REL_Y = 1,
+ REL_Z = 2,
+ REL_RX = 3,
+ REL_RY = 4,
+ REL_RZ = 5,
+ REL_HWHEEL = 6,
+ REL_DIAL = 7,
+ REL_WHEEL = 8,
+ REL_MISC = 9,
+ REL_RESERVED = 10,
+ REL_WHEEL_HI_RES = 11,
+ REL_HWHEEL_HI_RES = 12,
+ REL_MAX = 15,
+}
+
+pub const fn int_to_ev_rel(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_REL::REL_X),
+ 1 => Some(EV_REL::REL_Y),
+ 2 => Some(EV_REL::REL_Z),
+ 3 => Some(EV_REL::REL_RX),
+ 4 => Some(EV_REL::REL_RY),
+ 5 => Some(EV_REL::REL_RZ),
+ 6 => Some(EV_REL::REL_HWHEEL),
+ 7 => Some(EV_REL::REL_DIAL),
+ 8 => Some(EV_REL::REL_WHEEL),
+ 9 => Some(EV_REL::REL_MISC),
+ 10 => Some(EV_REL::REL_RESERVED),
+ 11 => Some(EV_REL::REL_WHEEL_HI_RES),
+ 12 => Some(EV_REL::REL_HWHEEL_HI_RES),
+ 15 => Some(EV_REL::REL_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_REL {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "REL_X" => Ok(EV_REL::REL_X),
+ "REL_Y" => Ok(EV_REL::REL_Y),
+ "REL_Z" => Ok(EV_REL::REL_Z),
+ "REL_RX" => Ok(EV_REL::REL_RX),
+ "REL_RY" => Ok(EV_REL::REL_RY),
+ "REL_RZ" => Ok(EV_REL::REL_RZ),
+ "REL_HWHEEL" => Ok(EV_REL::REL_HWHEEL),
+ "REL_DIAL" => Ok(EV_REL::REL_DIAL),
+ "REL_WHEEL" => Ok(EV_REL::REL_WHEEL),
+ "REL_MISC" => Ok(EV_REL::REL_MISC),
+ "REL_RESERVED" => Ok(EV_REL::REL_RESERVED),
+ "REL_WHEEL_HI_RES" => Ok(EV_REL::REL_WHEEL_HI_RES),
+ "REL_HWHEEL_HI_RES" => Ok(EV_REL::REL_HWHEEL_HI_RES),
+ "REL_MAX" => Ok(EV_REL::REL_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_REL {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_ABS {
+ ABS_X = 0,
+ ABS_Y = 1,
+ ABS_Z = 2,
+ ABS_RX = 3,
+ ABS_RY = 4,
+ ABS_RZ = 5,
+ ABS_THROTTLE = 6,
+ ABS_RUDDER = 7,
+ ABS_WHEEL = 8,
+ ABS_GAS = 9,
+ ABS_BRAKE = 10,
+ ABS_HAT0X = 16,
+ ABS_HAT0Y = 17,
+ ABS_HAT1X = 18,
+ ABS_HAT1Y = 19,
+ ABS_HAT2X = 20,
+ ABS_HAT2Y = 21,
+ ABS_HAT3X = 22,
+ ABS_HAT3Y = 23,
+ ABS_PRESSURE = 24,
+ ABS_DISTANCE = 25,
+ ABS_TILT_X = 26,
+ ABS_TILT_Y = 27,
+ ABS_TOOL_WIDTH = 28,
+ ABS_VOLUME = 32,
+ ABS_PROFILE = 33,
+ ABS_MISC = 40,
+ ABS_RESERVED = 46,
+ ABS_MT_SLOT = 47,
+ ABS_MT_TOUCH_MAJOR = 48,
+ ABS_MT_TOUCH_MINOR = 49,
+ ABS_MT_WIDTH_MAJOR = 50,
+ ABS_MT_WIDTH_MINOR = 51,
+ ABS_MT_ORIENTATION = 52,
+ ABS_MT_POSITION_X = 53,
+ ABS_MT_POSITION_Y = 54,
+ ABS_MT_TOOL_TYPE = 55,
+ ABS_MT_BLOB_ID = 56,
+ ABS_MT_TRACKING_ID = 57,
+ ABS_MT_PRESSURE = 58,
+ ABS_MT_DISTANCE = 59,
+ ABS_MT_TOOL_X = 60,
+ ABS_MT_TOOL_Y = 61,
+ ABS_MAX = 63,
+}
+
+pub const fn int_to_ev_abs(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_ABS::ABS_X),
+ 1 => Some(EV_ABS::ABS_Y),
+ 2 => Some(EV_ABS::ABS_Z),
+ 3 => Some(EV_ABS::ABS_RX),
+ 4 => Some(EV_ABS::ABS_RY),
+ 5 => Some(EV_ABS::ABS_RZ),
+ 6 => Some(EV_ABS::ABS_THROTTLE),
+ 7 => Some(EV_ABS::ABS_RUDDER),
+ 8 => Some(EV_ABS::ABS_WHEEL),
+ 9 => Some(EV_ABS::ABS_GAS),
+ 10 => Some(EV_ABS::ABS_BRAKE),
+ 16 => Some(EV_ABS::ABS_HAT0X),
+ 17 => Some(EV_ABS::ABS_HAT0Y),
+ 18 => Some(EV_ABS::ABS_HAT1X),
+ 19 => Some(EV_ABS::ABS_HAT1Y),
+ 20 => Some(EV_ABS::ABS_HAT2X),
+ 21 => Some(EV_ABS::ABS_HAT2Y),
+ 22 => Some(EV_ABS::ABS_HAT3X),
+ 23 => Some(EV_ABS::ABS_HAT3Y),
+ 24 => Some(EV_ABS::ABS_PRESSURE),
+ 25 => Some(EV_ABS::ABS_DISTANCE),
+ 26 => Some(EV_ABS::ABS_TILT_X),
+ 27 => Some(EV_ABS::ABS_TILT_Y),
+ 28 => Some(EV_ABS::ABS_TOOL_WIDTH),
+ 32 => Some(EV_ABS::ABS_VOLUME),
+ 33 => Some(EV_ABS::ABS_PROFILE),
+ 40 => Some(EV_ABS::ABS_MISC),
+ 46 => Some(EV_ABS::ABS_RESERVED),
+ 47 => Some(EV_ABS::ABS_MT_SLOT),
+ 48 => Some(EV_ABS::ABS_MT_TOUCH_MAJOR),
+ 49 => Some(EV_ABS::ABS_MT_TOUCH_MINOR),
+ 50 => Some(EV_ABS::ABS_MT_WIDTH_MAJOR),
+ 51 => Some(EV_ABS::ABS_MT_WIDTH_MINOR),
+ 52 => Some(EV_ABS::ABS_MT_ORIENTATION),
+ 53 => Some(EV_ABS::ABS_MT_POSITION_X),
+ 54 => Some(EV_ABS::ABS_MT_POSITION_Y),
+ 55 => Some(EV_ABS::ABS_MT_TOOL_TYPE),
+ 56 => Some(EV_ABS::ABS_MT_BLOB_ID),
+ 57 => Some(EV_ABS::ABS_MT_TRACKING_ID),
+ 58 => Some(EV_ABS::ABS_MT_PRESSURE),
+ 59 => Some(EV_ABS::ABS_MT_DISTANCE),
+ 60 => Some(EV_ABS::ABS_MT_TOOL_X),
+ 61 => Some(EV_ABS::ABS_MT_TOOL_Y),
+ 63 => Some(EV_ABS::ABS_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_ABS {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "ABS_X" => Ok(EV_ABS::ABS_X),
+ "ABS_Y" => Ok(EV_ABS::ABS_Y),
+ "ABS_Z" => Ok(EV_ABS::ABS_Z),
+ "ABS_RX" => Ok(EV_ABS::ABS_RX),
+ "ABS_RY" => Ok(EV_ABS::ABS_RY),
+ "ABS_RZ" => Ok(EV_ABS::ABS_RZ),
+ "ABS_THROTTLE" => Ok(EV_ABS::ABS_THROTTLE),
+ "ABS_RUDDER" => Ok(EV_ABS::ABS_RUDDER),
+ "ABS_WHEEL" => Ok(EV_ABS::ABS_WHEEL),
+ "ABS_GAS" => Ok(EV_ABS::ABS_GAS),
+ "ABS_BRAKE" => Ok(EV_ABS::ABS_BRAKE),
+ "ABS_HAT0X" => Ok(EV_ABS::ABS_HAT0X),
+ "ABS_HAT0Y" => Ok(EV_ABS::ABS_HAT0Y),
+ "ABS_HAT1X" => Ok(EV_ABS::ABS_HAT1X),
+ "ABS_HAT1Y" => Ok(EV_ABS::ABS_HAT1Y),
+ "ABS_HAT2X" => Ok(EV_ABS::ABS_HAT2X),
+ "ABS_HAT2Y" => Ok(EV_ABS::ABS_HAT2Y),
+ "ABS_HAT3X" => Ok(EV_ABS::ABS_HAT3X),
+ "ABS_HAT3Y" => Ok(EV_ABS::ABS_HAT3Y),
+ "ABS_PRESSURE" => Ok(EV_ABS::ABS_PRESSURE),
+ "ABS_DISTANCE" => Ok(EV_ABS::ABS_DISTANCE),
+ "ABS_TILT_X" => Ok(EV_ABS::ABS_TILT_X),
+ "ABS_TILT_Y" => Ok(EV_ABS::ABS_TILT_Y),
+ "ABS_TOOL_WIDTH" => Ok(EV_ABS::ABS_TOOL_WIDTH),
+ "ABS_VOLUME" => Ok(EV_ABS::ABS_VOLUME),
+ "ABS_PROFILE" => Ok(EV_ABS::ABS_PROFILE),
+ "ABS_MISC" => Ok(EV_ABS::ABS_MISC),
+ "ABS_RESERVED" => Ok(EV_ABS::ABS_RESERVED),
+ "ABS_MT_SLOT" => Ok(EV_ABS::ABS_MT_SLOT),
+ "ABS_MT_TOUCH_MAJOR" => Ok(EV_ABS::ABS_MT_TOUCH_MAJOR),
+ "ABS_MT_TOUCH_MINOR" => Ok(EV_ABS::ABS_MT_TOUCH_MINOR),
+ "ABS_MT_WIDTH_MAJOR" => Ok(EV_ABS::ABS_MT_WIDTH_MAJOR),
+ "ABS_MT_WIDTH_MINOR" => Ok(EV_ABS::ABS_MT_WIDTH_MINOR),
+ "ABS_MT_ORIENTATION" => Ok(EV_ABS::ABS_MT_ORIENTATION),
+ "ABS_MT_POSITION_X" => Ok(EV_ABS::ABS_MT_POSITION_X),
+ "ABS_MT_POSITION_Y" => Ok(EV_ABS::ABS_MT_POSITION_Y),
+ "ABS_MT_TOOL_TYPE" => Ok(EV_ABS::ABS_MT_TOOL_TYPE),
+ "ABS_MT_BLOB_ID" => Ok(EV_ABS::ABS_MT_BLOB_ID),
+ "ABS_MT_TRACKING_ID" => Ok(EV_ABS::ABS_MT_TRACKING_ID),
+ "ABS_MT_PRESSURE" => Ok(EV_ABS::ABS_MT_PRESSURE),
+ "ABS_MT_DISTANCE" => Ok(EV_ABS::ABS_MT_DISTANCE),
+ "ABS_MT_TOOL_X" => Ok(EV_ABS::ABS_MT_TOOL_X),
+ "ABS_MT_TOOL_Y" => Ok(EV_ABS::ABS_MT_TOOL_Y),
+ "ABS_MAX" => Ok(EV_ABS::ABS_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_ABS {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_KEY {
+ KEY_RESERVED = 0,
+ KEY_ESC = 1,
+ KEY_1 = 2,
+ KEY_2 = 3,
+ KEY_3 = 4,
+ KEY_4 = 5,
+ KEY_5 = 6,
+ KEY_6 = 7,
+ KEY_7 = 8,
+ KEY_8 = 9,
+ KEY_9 = 10,
+ KEY_0 = 11,
+ KEY_MINUS = 12,
+ KEY_EQUAL = 13,
+ KEY_BACKSPACE = 14,
+ KEY_TAB = 15,
+ KEY_Q = 16,
+ KEY_W = 17,
+ KEY_E = 18,
+ KEY_R = 19,
+ KEY_T = 20,
+ KEY_Y = 21,
+ KEY_U = 22,
+ KEY_I = 23,
+ KEY_O = 24,
+ KEY_P = 25,
+ KEY_LEFTBRACE = 26,
+ KEY_RIGHTBRACE = 27,
+ KEY_ENTER = 28,
+ KEY_LEFTCTRL = 29,
+ KEY_A = 30,
+ KEY_S = 31,
+ KEY_D = 32,
+ KEY_F = 33,
+ KEY_G = 34,
+ KEY_H = 35,
+ KEY_J = 36,
+ KEY_K = 37,
+ KEY_L = 38,
+ KEY_SEMICOLON = 39,
+ KEY_APOSTROPHE = 40,
+ KEY_GRAVE = 41,
+ KEY_LEFTSHIFT = 42,
+ KEY_BACKSLASH = 43,
+ KEY_Z = 44,
+ KEY_X = 45,
+ KEY_C = 46,
+ KEY_V = 47,
+ KEY_B = 48,
+ KEY_N = 49,
+ KEY_M = 50,
+ KEY_COMMA = 51,
+ KEY_DOT = 52,
+ KEY_SLASH = 53,
+ KEY_RIGHTSHIFT = 54,
+ KEY_KPASTERISK = 55,
+ KEY_LEFTALT = 56,
+ KEY_SPACE = 57,
+ KEY_CAPSLOCK = 58,
+ KEY_F1 = 59,
+ KEY_F2 = 60,
+ KEY_F3 = 61,
+ KEY_F4 = 62,
+ KEY_F5 = 63,
+ KEY_F6 = 64,
+ KEY_F7 = 65,
+ KEY_F8 = 66,
+ KEY_F9 = 67,
+ KEY_F10 = 68,
+ KEY_NUMLOCK = 69,
+ KEY_SCROLLLOCK = 70,
+ KEY_KP7 = 71,
+ KEY_KP8 = 72,
+ KEY_KP9 = 73,
+ KEY_KPMINUS = 74,
+ KEY_KP4 = 75,
+ KEY_KP5 = 76,
+ KEY_KP6 = 77,
+ KEY_KPPLUS = 78,
+ KEY_KP1 = 79,
+ KEY_KP2 = 80,
+ KEY_KP3 = 81,
+ KEY_KP0 = 82,
+ KEY_KPDOT = 83,
+ KEY_ZENKAKUHANKAKU = 85,
+ KEY_102ND = 86,
+ KEY_F11 = 87,
+ KEY_F12 = 88,
+ KEY_RO = 89,
+ KEY_KATAKANA = 90,
+ KEY_HIRAGANA = 91,
+ KEY_HENKAN = 92,
+ KEY_KATAKANAHIRAGANA = 93,
+ KEY_MUHENKAN = 94,
+ KEY_KPJPCOMMA = 95,
+ KEY_KPENTER = 96,
+ KEY_RIGHTCTRL = 97,
+ KEY_KPSLASH = 98,
+ KEY_SYSRQ = 99,
+ KEY_RIGHTALT = 100,
+ KEY_LINEFEED = 101,
+ KEY_HOME = 102,
+ KEY_UP = 103,
+ KEY_PAGEUP = 104,
+ KEY_LEFT = 105,
+ KEY_RIGHT = 106,
+ KEY_END = 107,
+ KEY_DOWN = 108,
+ KEY_PAGEDOWN = 109,
+ KEY_INSERT = 110,
+ KEY_DELETE = 111,
+ KEY_MACRO = 112,
+ KEY_MUTE = 113,
+ KEY_VOLUMEDOWN = 114,
+ KEY_VOLUMEUP = 115,
+ KEY_POWER = 116,
+ KEY_KPEQUAL = 117,
+ KEY_KPPLUSMINUS = 118,
+ KEY_PAUSE = 119,
+ KEY_SCALE = 120,
+ KEY_KPCOMMA = 121,
+ KEY_HANGEUL = 122,
+ KEY_HANJA = 123,
+ KEY_YEN = 124,
+ KEY_LEFTMETA = 125,
+ KEY_RIGHTMETA = 126,
+ KEY_COMPOSE = 127,
+ KEY_STOP = 128,
+ KEY_AGAIN = 129,
+ KEY_PROPS = 130,
+ KEY_UNDO = 131,
+ KEY_FRONT = 132,
+ KEY_COPY = 133,
+ KEY_OPEN = 134,
+ KEY_PASTE = 135,
+ KEY_FIND = 136,
+ KEY_CUT = 137,
+ KEY_HELP = 138,
+ KEY_MENU = 139,
+ KEY_CALC = 140,
+ KEY_SETUP = 141,
+ KEY_SLEEP = 142,
+ KEY_WAKEUP = 143,
+ KEY_FILE = 144,
+ KEY_SENDFILE = 145,
+ KEY_DELETEFILE = 146,
+ KEY_XFER = 147,
+ KEY_PROG1 = 148,
+ KEY_PROG2 = 149,
+ KEY_WWW = 150,
+ KEY_MSDOS = 151,
+ KEY_COFFEE = 152,
+ KEY_ROTATE_DISPLAY = 153,
+ KEY_CYCLEWINDOWS = 154,
+ KEY_MAIL = 155,
+ KEY_BOOKMARKS = 156,
+ KEY_COMPUTER = 157,
+ KEY_BACK = 158,
+ KEY_FORWARD = 159,
+ KEY_CLOSECD = 160,
+ KEY_EJECTCD = 161,
+ KEY_EJECTCLOSECD = 162,
+ KEY_NEXTSONG = 163,
+ KEY_PLAYPAUSE = 164,
+ KEY_PREVIOUSSONG = 165,
+ KEY_STOPCD = 166,
+ KEY_RECORD = 167,
+ KEY_REWIND = 168,
+ KEY_PHONE = 169,
+ KEY_ISO = 170,
+ KEY_CONFIG = 171,
+ KEY_HOMEPAGE = 172,
+ KEY_REFRESH = 173,
+ KEY_EXIT = 174,
+ KEY_MOVE = 175,
+ KEY_EDIT = 176,
+ KEY_SCROLLUP = 177,
+ KEY_SCROLLDOWN = 178,
+ KEY_KPLEFTPAREN = 179,
+ KEY_KPRIGHTPAREN = 180,
+ KEY_NEW = 181,
+ KEY_REDO = 182,
+ KEY_F13 = 183,
+ KEY_F14 = 184,
+ KEY_F15 = 185,
+ KEY_F16 = 186,
+ KEY_F17 = 187,
+ KEY_F18 = 188,
+ KEY_F19 = 189,
+ KEY_F20 = 190,
+ KEY_F21 = 191,
+ KEY_F22 = 192,
+ KEY_F23 = 193,
+ KEY_F24 = 194,
+ KEY_PLAYCD = 200,
+ KEY_PAUSECD = 201,
+ KEY_PROG3 = 202,
+ KEY_PROG4 = 203,
+ KEY_ALL_APPLICATIONS = 204,
+ KEY_SUSPEND = 205,
+ KEY_CLOSE = 206,
+ KEY_PLAY = 207,
+ KEY_FASTFORWARD = 208,
+ KEY_BASSBOOST = 209,
+ KEY_PRINT = 210,
+ KEY_HP = 211,
+ KEY_CAMERA = 212,
+ KEY_SOUND = 213,
+ KEY_QUESTION = 214,
+ KEY_EMAIL = 215,
+ KEY_CHAT = 216,
+ KEY_SEARCH = 217,
+ KEY_CONNECT = 218,
+ KEY_FINANCE = 219,
+ KEY_SPORT = 220,
+ KEY_SHOP = 221,
+ KEY_ALTERASE = 222,
+ KEY_CANCEL = 223,
+ KEY_BRIGHTNESSDOWN = 224,
+ KEY_BRIGHTNESSUP = 225,
+ KEY_MEDIA = 226,
+ KEY_SWITCHVIDEOMODE = 227,
+ KEY_KBDILLUMTOGGLE = 228,
+ KEY_KBDILLUMDOWN = 229,
+ KEY_KBDILLUMUP = 230,
+ KEY_SEND = 231,
+ KEY_REPLY = 232,
+ KEY_FORWARDMAIL = 233,
+ KEY_SAVE = 234,
+ KEY_DOCUMENTS = 235,
+ KEY_BATTERY = 236,
+ KEY_BLUETOOTH = 237,
+ KEY_WLAN = 238,
+ KEY_UWB = 239,
+ KEY_UNKNOWN = 240,
+ KEY_VIDEO_NEXT = 241,
+ KEY_VIDEO_PREV = 242,
+ KEY_BRIGHTNESS_CYCLE = 243,
+ KEY_BRIGHTNESS_AUTO = 244,
+ KEY_DISPLAY_OFF = 245,
+ KEY_WWAN = 246,
+ KEY_RFKILL = 247,
+ KEY_MICMUTE = 248,
+ KEY_OK = 352,
+ KEY_SELECT = 353,
+ KEY_GOTO = 354,
+ KEY_CLEAR = 355,
+ KEY_POWER2 = 356,
+ KEY_OPTION = 357,
+ KEY_INFO = 358,
+ KEY_TIME = 359,
+ KEY_VENDOR = 360,
+ KEY_ARCHIVE = 361,
+ KEY_PROGRAM = 362,
+ KEY_CHANNEL = 363,
+ KEY_FAVORITES = 364,
+ KEY_EPG = 365,
+ KEY_PVR = 366,
+ KEY_MHP = 367,
+ KEY_LANGUAGE = 368,
+ KEY_TITLE = 369,
+ KEY_SUBTITLE = 370,
+ KEY_ANGLE = 371,
+ KEY_FULL_SCREEN = 372,
+ KEY_MODE = 373,
+ KEY_KEYBOARD = 374,
+ KEY_ASPECT_RATIO = 375,
+ KEY_PC = 376,
+ KEY_TV = 377,
+ KEY_TV2 = 378,
+ KEY_VCR = 379,
+ KEY_VCR2 = 380,
+ KEY_SAT = 381,
+ KEY_SAT2 = 382,
+ KEY_CD = 383,
+ KEY_TAPE = 384,
+ KEY_RADIO = 385,
+ KEY_TUNER = 386,
+ KEY_PLAYER = 387,
+ KEY_TEXT = 388,
+ KEY_DVD = 389,
+ KEY_AUX = 390,
+ KEY_MP3 = 391,
+ KEY_AUDIO = 392,
+ KEY_VIDEO = 393,
+ KEY_DIRECTORY = 394,
+ KEY_LIST = 395,
+ KEY_MEMO = 396,
+ KEY_CALENDAR = 397,
+ KEY_RED = 398,
+ KEY_GREEN = 399,
+ KEY_YELLOW = 400,
+ KEY_BLUE = 401,
+ KEY_CHANNELUP = 402,
+ KEY_CHANNELDOWN = 403,
+ KEY_FIRST = 404,
+ KEY_LAST = 405,
+ KEY_AB = 406,
+ KEY_NEXT = 407,
+ KEY_RESTART = 408,
+ KEY_SLOW = 409,
+ KEY_SHUFFLE = 410,
+ KEY_BREAK = 411,
+ KEY_PREVIOUS = 412,
+ KEY_DIGITS = 413,
+ KEY_TEEN = 414,
+ KEY_TWEN = 415,
+ KEY_VIDEOPHONE = 416,
+ KEY_GAMES = 417,
+ KEY_ZOOMIN = 418,
+ KEY_ZOOMOUT = 419,
+ KEY_ZOOMRESET = 420,
+ KEY_WORDPROCESSOR = 421,
+ KEY_EDITOR = 422,
+ KEY_SPREADSHEET = 423,
+ KEY_GRAPHICSEDITOR = 424,
+ KEY_PRESENTATION = 425,
+ KEY_DATABASE = 426,
+ KEY_NEWS = 427,
+ KEY_VOICEMAIL = 428,
+ KEY_ADDRESSBOOK = 429,
+ KEY_MESSENGER = 430,
+ KEY_DISPLAYTOGGLE = 431,
+ KEY_SPELLCHECK = 432,
+ KEY_LOGOFF = 433,
+ KEY_DOLLAR = 434,
+ KEY_EURO = 435,
+ KEY_FRAMEBACK = 436,
+ KEY_FRAMEFORWARD = 437,
+ KEY_CONTEXT_MENU = 438,
+ KEY_MEDIA_REPEAT = 439,
+ KEY_10CHANNELSUP = 440,
+ KEY_10CHANNELSDOWN = 441,
+ KEY_IMAGES = 442,
+ KEY_NOTIFICATION_CENTER = 444,
+ KEY_PICKUP_PHONE = 445,
+ KEY_HANGUP_PHONE = 446,
+ KEY_DEL_EOL = 448,
+ KEY_DEL_EOS = 449,
+ KEY_INS_LINE = 450,
+ KEY_DEL_LINE = 451,
+ KEY_FN = 464,
+ KEY_FN_ESC = 465,
+ KEY_FN_F1 = 466,
+ KEY_FN_F2 = 467,
+ KEY_FN_F3 = 468,
+ KEY_FN_F4 = 469,
+ KEY_FN_F5 = 470,
+ KEY_FN_F6 = 471,
+ KEY_FN_F7 = 472,
+ KEY_FN_F8 = 473,
+ KEY_FN_F9 = 474,
+ KEY_FN_F10 = 475,
+ KEY_FN_F11 = 476,
+ KEY_FN_F12 = 477,
+ KEY_FN_1 = 478,
+ KEY_FN_2 = 479,
+ KEY_FN_D = 480,
+ KEY_FN_E = 481,
+ KEY_FN_F = 482,
+ KEY_FN_S = 483,
+ KEY_FN_B = 484,
+ KEY_FN_RIGHT_SHIFT = 485,
+ KEY_BRL_DOT1 = 497,
+ KEY_BRL_DOT2 = 498,
+ KEY_BRL_DOT3 = 499,
+ KEY_BRL_DOT4 = 500,
+ KEY_BRL_DOT5 = 501,
+ KEY_BRL_DOT6 = 502,
+ KEY_BRL_DOT7 = 503,
+ KEY_BRL_DOT8 = 504,
+ KEY_BRL_DOT9 = 505,
+ KEY_BRL_DOT10 = 506,
+ KEY_NUMERIC_0 = 512,
+ KEY_NUMERIC_1 = 513,
+ KEY_NUMERIC_2 = 514,
+ KEY_NUMERIC_3 = 515,
+ KEY_NUMERIC_4 = 516,
+ KEY_NUMERIC_5 = 517,
+ KEY_NUMERIC_6 = 518,
+ KEY_NUMERIC_7 = 519,
+ KEY_NUMERIC_8 = 520,
+ KEY_NUMERIC_9 = 521,
+ KEY_NUMERIC_STAR = 522,
+ KEY_NUMERIC_POUND = 523,
+ KEY_NUMERIC_A = 524,
+ KEY_NUMERIC_B = 525,
+ KEY_NUMERIC_C = 526,
+ KEY_NUMERIC_D = 527,
+ KEY_CAMERA_FOCUS = 528,
+ KEY_WPS_BUTTON = 529,
+ KEY_TOUCHPAD_TOGGLE = 530,
+ KEY_TOUCHPAD_ON = 531,
+ KEY_TOUCHPAD_OFF = 532,
+ KEY_CAMERA_ZOOMIN = 533,
+ KEY_CAMERA_ZOOMOUT = 534,
+ KEY_CAMERA_UP = 535,
+ KEY_CAMERA_DOWN = 536,
+ KEY_CAMERA_LEFT = 537,
+ KEY_CAMERA_RIGHT = 538,
+ KEY_ATTENDANT_ON = 539,
+ KEY_ATTENDANT_OFF = 540,
+ KEY_ATTENDANT_TOGGLE = 541,
+ KEY_LIGHTS_TOGGLE = 542,
+ KEY_ALS_TOGGLE = 560,
+ KEY_ROTATE_LOCK_TOGGLE = 561,
+ KEY_REFRESH_RATE_TOGGLE = 562,
+ KEY_BUTTONCONFIG = 576,
+ KEY_TASKMANAGER = 577,
+ KEY_JOURNAL = 578,
+ KEY_CONTROLPANEL = 579,
+ KEY_APPSELECT = 580,
+ KEY_SCREENSAVER = 581,
+ KEY_VOICECOMMAND = 582,
+ KEY_ASSISTANT = 583,
+ KEY_KBD_LAYOUT_NEXT = 584,
+ KEY_EMOJI_PICKER = 585,
+ KEY_DICTATE = 586,
+ KEY_CAMERA_ACCESS_ENABLE = 587,
+ KEY_CAMERA_ACCESS_DISABLE = 588,
+ KEY_CAMERA_ACCESS_TOGGLE = 589,
+ KEY_ACCESSIBILITY = 590,
+ KEY_DO_NOT_DISTURB = 591,
+ KEY_BRIGHTNESS_MIN = 592,
+ KEY_BRIGHTNESS_MAX = 593,
+ KEY_KBDINPUTASSIST_PREV = 608,
+ KEY_KBDINPUTASSIST_NEXT = 609,
+ KEY_KBDINPUTASSIST_PREVGROUP = 610,
+ KEY_KBDINPUTASSIST_NEXTGROUP = 611,
+ KEY_KBDINPUTASSIST_ACCEPT = 612,
+ KEY_KBDINPUTASSIST_CANCEL = 613,
+ KEY_RIGHT_UP = 614,
+ KEY_RIGHT_DOWN = 615,
+ KEY_LEFT_UP = 616,
+ KEY_LEFT_DOWN = 617,
+ KEY_ROOT_MENU = 618,
+ KEY_MEDIA_TOP_MENU = 619,
+ KEY_NUMERIC_11 = 620,
+ KEY_NUMERIC_12 = 621,
+ KEY_AUDIO_DESC = 622,
+ KEY_3D_MODE = 623,
+ KEY_NEXT_FAVORITE = 624,
+ KEY_STOP_RECORD = 625,
+ KEY_PAUSE_RECORD = 626,
+ KEY_VOD = 627,
+ KEY_UNMUTE = 628,
+ KEY_FASTREVERSE = 629,
+ KEY_SLOWREVERSE = 630,
+ KEY_DATA = 631,
+ KEY_ONSCREEN_KEYBOARD = 632,
+ KEY_PRIVACY_SCREEN_TOGGLE = 633,
+ KEY_SELECTIVE_SCREENSHOT = 634,
+ KEY_NEXT_ELEMENT = 635,
+ KEY_PREVIOUS_ELEMENT = 636,
+ KEY_AUTOPILOT_ENGAGE_TOGGLE = 637,
+ KEY_MARK_WAYPOINT = 638,
+ KEY_SOS = 639,
+ KEY_NAV_CHART = 640,
+ KEY_FISHING_CHART = 641,
+ KEY_SINGLE_RANGE_RADAR = 642,
+ KEY_DUAL_RANGE_RADAR = 643,
+ KEY_RADAR_OVERLAY = 644,
+ KEY_TRADITIONAL_SONAR = 645,
+ KEY_CLEARVU_SONAR = 646,
+ KEY_SIDEVU_SONAR = 647,
+ KEY_NAV_INFO = 648,
+ KEY_BRIGHTNESS_MENU = 649,
+ KEY_MACRO1 = 656,
+ KEY_MACRO2 = 657,
+ KEY_MACRO3 = 658,
+ KEY_MACRO4 = 659,
+ KEY_MACRO5 = 660,
+ KEY_MACRO6 = 661,
+ KEY_MACRO7 = 662,
+ KEY_MACRO8 = 663,
+ KEY_MACRO9 = 664,
+ KEY_MACRO10 = 665,
+ KEY_MACRO11 = 666,
+ KEY_MACRO12 = 667,
+ KEY_MACRO13 = 668,
+ KEY_MACRO14 = 669,
+ KEY_MACRO15 = 670,
+ KEY_MACRO16 = 671,
+ KEY_MACRO17 = 672,
+ KEY_MACRO18 = 673,
+ KEY_MACRO19 = 674,
+ KEY_MACRO20 = 675,
+ KEY_MACRO21 = 676,
+ KEY_MACRO22 = 677,
+ KEY_MACRO23 = 678,
+ KEY_MACRO24 = 679,
+ KEY_MACRO25 = 680,
+ KEY_MACRO26 = 681,
+ KEY_MACRO27 = 682,
+ KEY_MACRO28 = 683,
+ KEY_MACRO29 = 684,
+ KEY_MACRO30 = 685,
+ KEY_MACRO_RECORD_START = 688,
+ KEY_MACRO_RECORD_STOP = 689,
+ KEY_MACRO_PRESET_CYCLE = 690,
+ KEY_MACRO_PRESET1 = 691,
+ KEY_MACRO_PRESET2 = 692,
+ KEY_MACRO_PRESET3 = 693,
+ KEY_KBD_LCD_MENU1 = 696,
+ KEY_KBD_LCD_MENU2 = 697,
+ KEY_KBD_LCD_MENU3 = 698,
+ KEY_KBD_LCD_MENU4 = 699,
+ KEY_KBD_LCD_MENU5 = 700,
+ KEY_MAX = 767,
+ BTN_0 = 256,
+ BTN_1 = 257,
+ BTN_2 = 258,
+ BTN_3 = 259,
+ BTN_4 = 260,
+ BTN_5 = 261,
+ BTN_6 = 262,
+ BTN_7 = 263,
+ BTN_8 = 264,
+ BTN_9 = 265,
+ BTN_LEFT = 272,
+ BTN_RIGHT = 273,
+ BTN_MIDDLE = 274,
+ BTN_SIDE = 275,
+ BTN_EXTRA = 276,
+ BTN_FORWARD = 277,
+ BTN_BACK = 278,
+ BTN_TASK = 279,
+ BTN_TRIGGER = 288,
+ BTN_THUMB = 289,
+ BTN_THUMB2 = 290,
+ BTN_TOP = 291,
+ BTN_TOP2 = 292,
+ BTN_PINKIE = 293,
+ BTN_BASE = 294,
+ BTN_BASE2 = 295,
+ BTN_BASE3 = 296,
+ BTN_BASE4 = 297,
+ BTN_BASE5 = 298,
+ BTN_BASE6 = 299,
+ BTN_DEAD = 303,
+ BTN_SOUTH = 304,
+ BTN_EAST = 305,
+ BTN_C = 306,
+ BTN_NORTH = 307,
+ BTN_WEST = 308,
+ BTN_Z = 309,
+ BTN_TL = 310,
+ BTN_TR = 311,
+ BTN_TL2 = 312,
+ BTN_TR2 = 313,
+ BTN_SELECT = 314,
+ BTN_START = 315,
+ BTN_MODE = 316,
+ BTN_THUMBL = 317,
+ BTN_THUMBR = 318,
+ BTN_TOOL_PEN = 320,
+ BTN_TOOL_RUBBER = 321,
+ BTN_TOOL_BRUSH = 322,
+ BTN_TOOL_PENCIL = 323,
+ BTN_TOOL_AIRBRUSH = 324,
+ BTN_TOOL_FINGER = 325,
+ BTN_TOOL_MOUSE = 326,
+ BTN_TOOL_LENS = 327,
+ BTN_TOOL_QUINTTAP = 328,
+ BTN_STYLUS3 = 329,
+ BTN_TOUCH = 330,
+ BTN_STYLUS = 331,
+ BTN_STYLUS2 = 332,
+ BTN_TOOL_DOUBLETAP = 333,
+ BTN_TOOL_TRIPLETAP = 334,
+ BTN_TOOL_QUADTAP = 335,
+ BTN_GEAR_DOWN = 336,
+ BTN_GEAR_UP = 337,
+ BTN_DPAD_UP = 544,
+ BTN_DPAD_DOWN = 545,
+ BTN_DPAD_LEFT = 546,
+ BTN_DPAD_RIGHT = 547,
+ BTN_TRIGGER_HAPPY1 = 704,
+ BTN_TRIGGER_HAPPY2 = 705,
+ BTN_TRIGGER_HAPPY3 = 706,
+ BTN_TRIGGER_HAPPY4 = 707,
+ BTN_TRIGGER_HAPPY5 = 708,
+ BTN_TRIGGER_HAPPY6 = 709,
+ BTN_TRIGGER_HAPPY7 = 710,
+ BTN_TRIGGER_HAPPY8 = 711,
+ BTN_TRIGGER_HAPPY9 = 712,
+ BTN_TRIGGER_HAPPY10 = 713,
+ BTN_TRIGGER_HAPPY11 = 714,
+ BTN_TRIGGER_HAPPY12 = 715,
+ BTN_TRIGGER_HAPPY13 = 716,
+ BTN_TRIGGER_HAPPY14 = 717,
+ BTN_TRIGGER_HAPPY15 = 718,
+ BTN_TRIGGER_HAPPY16 = 719,
+ BTN_TRIGGER_HAPPY17 = 720,
+ BTN_TRIGGER_HAPPY18 = 721,
+ BTN_TRIGGER_HAPPY19 = 722,
+ BTN_TRIGGER_HAPPY20 = 723,
+ BTN_TRIGGER_HAPPY21 = 724,
+ BTN_TRIGGER_HAPPY22 = 725,
+ BTN_TRIGGER_HAPPY23 = 726,
+ BTN_TRIGGER_HAPPY24 = 727,
+ BTN_TRIGGER_HAPPY25 = 728,
+ BTN_TRIGGER_HAPPY26 = 729,
+ BTN_TRIGGER_HAPPY27 = 730,
+ BTN_TRIGGER_HAPPY28 = 731,
+ BTN_TRIGGER_HAPPY29 = 732,
+ BTN_TRIGGER_HAPPY30 = 733,
+ BTN_TRIGGER_HAPPY31 = 734,
+ BTN_TRIGGER_HAPPY32 = 735,
+ BTN_TRIGGER_HAPPY33 = 736,
+ BTN_TRIGGER_HAPPY34 = 737,
+ BTN_TRIGGER_HAPPY35 = 738,
+ BTN_TRIGGER_HAPPY36 = 739,
+ BTN_TRIGGER_HAPPY37 = 740,
+ BTN_TRIGGER_HAPPY38 = 741,
+ BTN_TRIGGER_HAPPY39 = 742,
+ BTN_TRIGGER_HAPPY40 = 743,
+}
+
+pub const fn int_to_ev_key(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_KEY::KEY_RESERVED),
+ 1 => Some(EV_KEY::KEY_ESC),
+ 2 => Some(EV_KEY::KEY_1),
+ 3 => Some(EV_KEY::KEY_2),
+ 4 => Some(EV_KEY::KEY_3),
+ 5 => Some(EV_KEY::KEY_4),
+ 6 => Some(EV_KEY::KEY_5),
+ 7 => Some(EV_KEY::KEY_6),
+ 8 => Some(EV_KEY::KEY_7),
+ 9 => Some(EV_KEY::KEY_8),
+ 10 => Some(EV_KEY::KEY_9),
+ 11 => Some(EV_KEY::KEY_0),
+ 12 => Some(EV_KEY::KEY_MINUS),
+ 13 => Some(EV_KEY::KEY_EQUAL),
+ 14 => Some(EV_KEY::KEY_BACKSPACE),
+ 15 => Some(EV_KEY::KEY_TAB),
+ 16 => Some(EV_KEY::KEY_Q),
+ 17 => Some(EV_KEY::KEY_W),
+ 18 => Some(EV_KEY::KEY_E),
+ 19 => Some(EV_KEY::KEY_R),
+ 20 => Some(EV_KEY::KEY_T),
+ 21 => Some(EV_KEY::KEY_Y),
+ 22 => Some(EV_KEY::KEY_U),
+ 23 => Some(EV_KEY::KEY_I),
+ 24 => Some(EV_KEY::KEY_O),
+ 25 => Some(EV_KEY::KEY_P),
+ 26 => Some(EV_KEY::KEY_LEFTBRACE),
+ 27 => Some(EV_KEY::KEY_RIGHTBRACE),
+ 28 => Some(EV_KEY::KEY_ENTER),
+ 29 => Some(EV_KEY::KEY_LEFTCTRL),
+ 30 => Some(EV_KEY::KEY_A),
+ 31 => Some(EV_KEY::KEY_S),
+ 32 => Some(EV_KEY::KEY_D),
+ 33 => Some(EV_KEY::KEY_F),
+ 34 => Some(EV_KEY::KEY_G),
+ 35 => Some(EV_KEY::KEY_H),
+ 36 => Some(EV_KEY::KEY_J),
+ 37 => Some(EV_KEY::KEY_K),
+ 38 => Some(EV_KEY::KEY_L),
+ 39 => Some(EV_KEY::KEY_SEMICOLON),
+ 40 => Some(EV_KEY::KEY_APOSTROPHE),
+ 41 => Some(EV_KEY::KEY_GRAVE),
+ 42 => Some(EV_KEY::KEY_LEFTSHIFT),
+ 43 => Some(EV_KEY::KEY_BACKSLASH),
+ 44 => Some(EV_KEY::KEY_Z),
+ 45 => Some(EV_KEY::KEY_X),
+ 46 => Some(EV_KEY::KEY_C),
+ 47 => Some(EV_KEY::KEY_V),
+ 48 => Some(EV_KEY::KEY_B),
+ 49 => Some(EV_KEY::KEY_N),
+ 50 => Some(EV_KEY::KEY_M),
+ 51 => Some(EV_KEY::KEY_COMMA),
+ 52 => Some(EV_KEY::KEY_DOT),
+ 53 => Some(EV_KEY::KEY_SLASH),
+ 54 => Some(EV_KEY::KEY_RIGHTSHIFT),
+ 55 => Some(EV_KEY::KEY_KPASTERISK),
+ 56 => Some(EV_KEY::KEY_LEFTALT),
+ 57 => Some(EV_KEY::KEY_SPACE),
+ 58 => Some(EV_KEY::KEY_CAPSLOCK),
+ 59 => Some(EV_KEY::KEY_F1),
+ 60 => Some(EV_KEY::KEY_F2),
+ 61 => Some(EV_KEY::KEY_F3),
+ 62 => Some(EV_KEY::KEY_F4),
+ 63 => Some(EV_KEY::KEY_F5),
+ 64 => Some(EV_KEY::KEY_F6),
+ 65 => Some(EV_KEY::KEY_F7),
+ 66 => Some(EV_KEY::KEY_F8),
+ 67 => Some(EV_KEY::KEY_F9),
+ 68 => Some(EV_KEY::KEY_F10),
+ 69 => Some(EV_KEY::KEY_NUMLOCK),
+ 70 => Some(EV_KEY::KEY_SCROLLLOCK),
+ 71 => Some(EV_KEY::KEY_KP7),
+ 72 => Some(EV_KEY::KEY_KP8),
+ 73 => Some(EV_KEY::KEY_KP9),
+ 74 => Some(EV_KEY::KEY_KPMINUS),
+ 75 => Some(EV_KEY::KEY_KP4),
+ 76 => Some(EV_KEY::KEY_KP5),
+ 77 => Some(EV_KEY::KEY_KP6),
+ 78 => Some(EV_KEY::KEY_KPPLUS),
+ 79 => Some(EV_KEY::KEY_KP1),
+ 80 => Some(EV_KEY::KEY_KP2),
+ 81 => Some(EV_KEY::KEY_KP3),
+ 82 => Some(EV_KEY::KEY_KP0),
+ 83 => Some(EV_KEY::KEY_KPDOT),
+ 85 => Some(EV_KEY::KEY_ZENKAKUHANKAKU),
+ 86 => Some(EV_KEY::KEY_102ND),
+ 87 => Some(EV_KEY::KEY_F11),
+ 88 => Some(EV_KEY::KEY_F12),
+ 89 => Some(EV_KEY::KEY_RO),
+ 90 => Some(EV_KEY::KEY_KATAKANA),
+ 91 => Some(EV_KEY::KEY_HIRAGANA),
+ 92 => Some(EV_KEY::KEY_HENKAN),
+ 93 => Some(EV_KEY::KEY_KATAKANAHIRAGANA),
+ 94 => Some(EV_KEY::KEY_MUHENKAN),
+ 95 => Some(EV_KEY::KEY_KPJPCOMMA),
+ 96 => Some(EV_KEY::KEY_KPENTER),
+ 97 => Some(EV_KEY::KEY_RIGHTCTRL),
+ 98 => Some(EV_KEY::KEY_KPSLASH),
+ 99 => Some(EV_KEY::KEY_SYSRQ),
+ 100 => Some(EV_KEY::KEY_RIGHTALT),
+ 101 => Some(EV_KEY::KEY_LINEFEED),
+ 102 => Some(EV_KEY::KEY_HOME),
+ 103 => Some(EV_KEY::KEY_UP),
+ 104 => Some(EV_KEY::KEY_PAGEUP),
+ 105 => Some(EV_KEY::KEY_LEFT),
+ 106 => Some(EV_KEY::KEY_RIGHT),
+ 107 => Some(EV_KEY::KEY_END),
+ 108 => Some(EV_KEY::KEY_DOWN),
+ 109 => Some(EV_KEY::KEY_PAGEDOWN),
+ 110 => Some(EV_KEY::KEY_INSERT),
+ 111 => Some(EV_KEY::KEY_DELETE),
+ 112 => Some(EV_KEY::KEY_MACRO),
+ 113 => Some(EV_KEY::KEY_MUTE),
+ 114 => Some(EV_KEY::KEY_VOLUMEDOWN),
+ 115 => Some(EV_KEY::KEY_VOLUMEUP),
+ 116 => Some(EV_KEY::KEY_POWER),
+ 117 => Some(EV_KEY::KEY_KPEQUAL),
+ 118 => Some(EV_KEY::KEY_KPPLUSMINUS),
+ 119 => Some(EV_KEY::KEY_PAUSE),
+ 120 => Some(EV_KEY::KEY_SCALE),
+ 121 => Some(EV_KEY::KEY_KPCOMMA),
+ 122 => Some(EV_KEY::KEY_HANGEUL),
+ 123 => Some(EV_KEY::KEY_HANJA),
+ 124 => Some(EV_KEY::KEY_YEN),
+ 125 => Some(EV_KEY::KEY_LEFTMETA),
+ 126 => Some(EV_KEY::KEY_RIGHTMETA),
+ 127 => Some(EV_KEY::KEY_COMPOSE),
+ 128 => Some(EV_KEY::KEY_STOP),
+ 129 => Some(EV_KEY::KEY_AGAIN),
+ 130 => Some(EV_KEY::KEY_PROPS),
+ 131 => Some(EV_KEY::KEY_UNDO),
+ 132 => Some(EV_KEY::KEY_FRONT),
+ 133 => Some(EV_KEY::KEY_COPY),
+ 134 => Some(EV_KEY::KEY_OPEN),
+ 135 => Some(EV_KEY::KEY_PASTE),
+ 136 => Some(EV_KEY::KEY_FIND),
+ 137 => Some(EV_KEY::KEY_CUT),
+ 138 => Some(EV_KEY::KEY_HELP),
+ 139 => Some(EV_KEY::KEY_MENU),
+ 140 => Some(EV_KEY::KEY_CALC),
+ 141 => Some(EV_KEY::KEY_SETUP),
+ 142 => Some(EV_KEY::KEY_SLEEP),
+ 143 => Some(EV_KEY::KEY_WAKEUP),
+ 144 => Some(EV_KEY::KEY_FILE),
+ 145 => Some(EV_KEY::KEY_SENDFILE),
+ 146 => Some(EV_KEY::KEY_DELETEFILE),
+ 147 => Some(EV_KEY::KEY_XFER),
+ 148 => Some(EV_KEY::KEY_PROG1),
+ 149 => Some(EV_KEY::KEY_PROG2),
+ 150 => Some(EV_KEY::KEY_WWW),
+ 151 => Some(EV_KEY::KEY_MSDOS),
+ 152 => Some(EV_KEY::KEY_COFFEE),
+ 153 => Some(EV_KEY::KEY_ROTATE_DISPLAY),
+ 154 => Some(EV_KEY::KEY_CYCLEWINDOWS),
+ 155 => Some(EV_KEY::KEY_MAIL),
+ 156 => Some(EV_KEY::KEY_BOOKMARKS),
+ 157 => Some(EV_KEY::KEY_COMPUTER),
+ 158 => Some(EV_KEY::KEY_BACK),
+ 159 => Some(EV_KEY::KEY_FORWARD),
+ 160 => Some(EV_KEY::KEY_CLOSECD),
+ 161 => Some(EV_KEY::KEY_EJECTCD),
+ 162 => Some(EV_KEY::KEY_EJECTCLOSECD),
+ 163 => Some(EV_KEY::KEY_NEXTSONG),
+ 164 => Some(EV_KEY::KEY_PLAYPAUSE),
+ 165 => Some(EV_KEY::KEY_PREVIOUSSONG),
+ 166 => Some(EV_KEY::KEY_STOPCD),
+ 167 => Some(EV_KEY::KEY_RECORD),
+ 168 => Some(EV_KEY::KEY_REWIND),
+ 169 => Some(EV_KEY::KEY_PHONE),
+ 170 => Some(EV_KEY::KEY_ISO),
+ 171 => Some(EV_KEY::KEY_CONFIG),
+ 172 => Some(EV_KEY::KEY_HOMEPAGE),
+ 173 => Some(EV_KEY::KEY_REFRESH),
+ 174 => Some(EV_KEY::KEY_EXIT),
+ 175 => Some(EV_KEY::KEY_MOVE),
+ 176 => Some(EV_KEY::KEY_EDIT),
+ 177 => Some(EV_KEY::KEY_SCROLLUP),
+ 178 => Some(EV_KEY::KEY_SCROLLDOWN),
+ 179 => Some(EV_KEY::KEY_KPLEFTPAREN),
+ 180 => Some(EV_KEY::KEY_KPRIGHTPAREN),
+ 181 => Some(EV_KEY::KEY_NEW),
+ 182 => Some(EV_KEY::KEY_REDO),
+ 183 => Some(EV_KEY::KEY_F13),
+ 184 => Some(EV_KEY::KEY_F14),
+ 185 => Some(EV_KEY::KEY_F15),
+ 186 => Some(EV_KEY::KEY_F16),
+ 187 => Some(EV_KEY::KEY_F17),
+ 188 => Some(EV_KEY::KEY_F18),
+ 189 => Some(EV_KEY::KEY_F19),
+ 190 => Some(EV_KEY::KEY_F20),
+ 191 => Some(EV_KEY::KEY_F21),
+ 192 => Some(EV_KEY::KEY_F22),
+ 193 => Some(EV_KEY::KEY_F23),
+ 194 => Some(EV_KEY::KEY_F24),
+ 200 => Some(EV_KEY::KEY_PLAYCD),
+ 201 => Some(EV_KEY::KEY_PAUSECD),
+ 202 => Some(EV_KEY::KEY_PROG3),
+ 203 => Some(EV_KEY::KEY_PROG4),
+ 204 => Some(EV_KEY::KEY_ALL_APPLICATIONS),
+ 205 => Some(EV_KEY::KEY_SUSPEND),
+ 206 => Some(EV_KEY::KEY_CLOSE),
+ 207 => Some(EV_KEY::KEY_PLAY),
+ 208 => Some(EV_KEY::KEY_FASTFORWARD),
+ 209 => Some(EV_KEY::KEY_BASSBOOST),
+ 210 => Some(EV_KEY::KEY_PRINT),
+ 211 => Some(EV_KEY::KEY_HP),
+ 212 => Some(EV_KEY::KEY_CAMERA),
+ 213 => Some(EV_KEY::KEY_SOUND),
+ 214 => Some(EV_KEY::KEY_QUESTION),
+ 215 => Some(EV_KEY::KEY_EMAIL),
+ 216 => Some(EV_KEY::KEY_CHAT),
+ 217 => Some(EV_KEY::KEY_SEARCH),
+ 218 => Some(EV_KEY::KEY_CONNECT),
+ 219 => Some(EV_KEY::KEY_FINANCE),
+ 220 => Some(EV_KEY::KEY_SPORT),
+ 221 => Some(EV_KEY::KEY_SHOP),
+ 222 => Some(EV_KEY::KEY_ALTERASE),
+ 223 => Some(EV_KEY::KEY_CANCEL),
+ 224 => Some(EV_KEY::KEY_BRIGHTNESSDOWN),
+ 225 => Some(EV_KEY::KEY_BRIGHTNESSUP),
+ 226 => Some(EV_KEY::KEY_MEDIA),
+ 227 => Some(EV_KEY::KEY_SWITCHVIDEOMODE),
+ 228 => Some(EV_KEY::KEY_KBDILLUMTOGGLE),
+ 229 => Some(EV_KEY::KEY_KBDILLUMDOWN),
+ 230 => Some(EV_KEY::KEY_KBDILLUMUP),
+ 231 => Some(EV_KEY::KEY_SEND),
+ 232 => Some(EV_KEY::KEY_REPLY),
+ 233 => Some(EV_KEY::KEY_FORWARDMAIL),
+ 234 => Some(EV_KEY::KEY_SAVE),
+ 235 => Some(EV_KEY::KEY_DOCUMENTS),
+ 236 => Some(EV_KEY::KEY_BATTERY),
+ 237 => Some(EV_KEY::KEY_BLUETOOTH),
+ 238 => Some(EV_KEY::KEY_WLAN),
+ 239 => Some(EV_KEY::KEY_UWB),
+ 240 => Some(EV_KEY::KEY_UNKNOWN),
+ 241 => Some(EV_KEY::KEY_VIDEO_NEXT),
+ 242 => Some(EV_KEY::KEY_VIDEO_PREV),
+ 243 => Some(EV_KEY::KEY_BRIGHTNESS_CYCLE),
+ 244 => Some(EV_KEY::KEY_BRIGHTNESS_AUTO),
+ 245 => Some(EV_KEY::KEY_DISPLAY_OFF),
+ 246 => Some(EV_KEY::KEY_WWAN),
+ 247 => Some(EV_KEY::KEY_RFKILL),
+ 248 => Some(EV_KEY::KEY_MICMUTE),
+ 352 => Some(EV_KEY::KEY_OK),
+ 353 => Some(EV_KEY::KEY_SELECT),
+ 354 => Some(EV_KEY::KEY_GOTO),
+ 355 => Some(EV_KEY::KEY_CLEAR),
+ 356 => Some(EV_KEY::KEY_POWER2),
+ 357 => Some(EV_KEY::KEY_OPTION),
+ 358 => Some(EV_KEY::KEY_INFO),
+ 359 => Some(EV_KEY::KEY_TIME),
+ 360 => Some(EV_KEY::KEY_VENDOR),
+ 361 => Some(EV_KEY::KEY_ARCHIVE),
+ 362 => Some(EV_KEY::KEY_PROGRAM),
+ 363 => Some(EV_KEY::KEY_CHANNEL),
+ 364 => Some(EV_KEY::KEY_FAVORITES),
+ 365 => Some(EV_KEY::KEY_EPG),
+ 366 => Some(EV_KEY::KEY_PVR),
+ 367 => Some(EV_KEY::KEY_MHP),
+ 368 => Some(EV_KEY::KEY_LANGUAGE),
+ 369 => Some(EV_KEY::KEY_TITLE),
+ 370 => Some(EV_KEY::KEY_SUBTITLE),
+ 371 => Some(EV_KEY::KEY_ANGLE),
+ 372 => Some(EV_KEY::KEY_FULL_SCREEN),
+ 373 => Some(EV_KEY::KEY_MODE),
+ 374 => Some(EV_KEY::KEY_KEYBOARD),
+ 375 => Some(EV_KEY::KEY_ASPECT_RATIO),
+ 376 => Some(EV_KEY::KEY_PC),
+ 377 => Some(EV_KEY::KEY_TV),
+ 378 => Some(EV_KEY::KEY_TV2),
+ 379 => Some(EV_KEY::KEY_VCR),
+ 380 => Some(EV_KEY::KEY_VCR2),
+ 381 => Some(EV_KEY::KEY_SAT),
+ 382 => Some(EV_KEY::KEY_SAT2),
+ 383 => Some(EV_KEY::KEY_CD),
+ 384 => Some(EV_KEY::KEY_TAPE),
+ 385 => Some(EV_KEY::KEY_RADIO),
+ 386 => Some(EV_KEY::KEY_TUNER),
+ 387 => Some(EV_KEY::KEY_PLAYER),
+ 388 => Some(EV_KEY::KEY_TEXT),
+ 389 => Some(EV_KEY::KEY_DVD),
+ 390 => Some(EV_KEY::KEY_AUX),
+ 391 => Some(EV_KEY::KEY_MP3),
+ 392 => Some(EV_KEY::KEY_AUDIO),
+ 393 => Some(EV_KEY::KEY_VIDEO),
+ 394 => Some(EV_KEY::KEY_DIRECTORY),
+ 395 => Some(EV_KEY::KEY_LIST),
+ 396 => Some(EV_KEY::KEY_MEMO),
+ 397 => Some(EV_KEY::KEY_CALENDAR),
+ 398 => Some(EV_KEY::KEY_RED),
+ 399 => Some(EV_KEY::KEY_GREEN),
+ 400 => Some(EV_KEY::KEY_YELLOW),
+ 401 => Some(EV_KEY::KEY_BLUE),
+ 402 => Some(EV_KEY::KEY_CHANNELUP),
+ 403 => Some(EV_KEY::KEY_CHANNELDOWN),
+ 404 => Some(EV_KEY::KEY_FIRST),
+ 405 => Some(EV_KEY::KEY_LAST),
+ 406 => Some(EV_KEY::KEY_AB),
+ 407 => Some(EV_KEY::KEY_NEXT),
+ 408 => Some(EV_KEY::KEY_RESTART),
+ 409 => Some(EV_KEY::KEY_SLOW),
+ 410 => Some(EV_KEY::KEY_SHUFFLE),
+ 411 => Some(EV_KEY::KEY_BREAK),
+ 412 => Some(EV_KEY::KEY_PREVIOUS),
+ 413 => Some(EV_KEY::KEY_DIGITS),
+ 414 => Some(EV_KEY::KEY_TEEN),
+ 415 => Some(EV_KEY::KEY_TWEN),
+ 416 => Some(EV_KEY::KEY_VIDEOPHONE),
+ 417 => Some(EV_KEY::KEY_GAMES),
+ 418 => Some(EV_KEY::KEY_ZOOMIN),
+ 419 => Some(EV_KEY::KEY_ZOOMOUT),
+ 420 => Some(EV_KEY::KEY_ZOOMRESET),
+ 421 => Some(EV_KEY::KEY_WORDPROCESSOR),
+ 422 => Some(EV_KEY::KEY_EDITOR),
+ 423 => Some(EV_KEY::KEY_SPREADSHEET),
+ 424 => Some(EV_KEY::KEY_GRAPHICSEDITOR),
+ 425 => Some(EV_KEY::KEY_PRESENTATION),
+ 426 => Some(EV_KEY::KEY_DATABASE),
+ 427 => Some(EV_KEY::KEY_NEWS),
+ 428 => Some(EV_KEY::KEY_VOICEMAIL),
+ 429 => Some(EV_KEY::KEY_ADDRESSBOOK),
+ 430 => Some(EV_KEY::KEY_MESSENGER),
+ 431 => Some(EV_KEY::KEY_DISPLAYTOGGLE),
+ 432 => Some(EV_KEY::KEY_SPELLCHECK),
+ 433 => Some(EV_KEY::KEY_LOGOFF),
+ 434 => Some(EV_KEY::KEY_DOLLAR),
+ 435 => Some(EV_KEY::KEY_EURO),
+ 436 => Some(EV_KEY::KEY_FRAMEBACK),
+ 437 => Some(EV_KEY::KEY_FRAMEFORWARD),
+ 438 => Some(EV_KEY::KEY_CONTEXT_MENU),
+ 439 => Some(EV_KEY::KEY_MEDIA_REPEAT),
+ 440 => Some(EV_KEY::KEY_10CHANNELSUP),
+ 441 => Some(EV_KEY::KEY_10CHANNELSDOWN),
+ 442 => Some(EV_KEY::KEY_IMAGES),
+ 444 => Some(EV_KEY::KEY_NOTIFICATION_CENTER),
+ 445 => Some(EV_KEY::KEY_PICKUP_PHONE),
+ 446 => Some(EV_KEY::KEY_HANGUP_PHONE),
+ 448 => Some(EV_KEY::KEY_DEL_EOL),
+ 449 => Some(EV_KEY::KEY_DEL_EOS),
+ 450 => Some(EV_KEY::KEY_INS_LINE),
+ 451 => Some(EV_KEY::KEY_DEL_LINE),
+ 464 => Some(EV_KEY::KEY_FN),
+ 465 => Some(EV_KEY::KEY_FN_ESC),
+ 466 => Some(EV_KEY::KEY_FN_F1),
+ 467 => Some(EV_KEY::KEY_FN_F2),
+ 468 => Some(EV_KEY::KEY_FN_F3),
+ 469 => Some(EV_KEY::KEY_FN_F4),
+ 470 => Some(EV_KEY::KEY_FN_F5),
+ 471 => Some(EV_KEY::KEY_FN_F6),
+ 472 => Some(EV_KEY::KEY_FN_F7),
+ 473 => Some(EV_KEY::KEY_FN_F8),
+ 474 => Some(EV_KEY::KEY_FN_F9),
+ 475 => Some(EV_KEY::KEY_FN_F10),
+ 476 => Some(EV_KEY::KEY_FN_F11),
+ 477 => Some(EV_KEY::KEY_FN_F12),
+ 478 => Some(EV_KEY::KEY_FN_1),
+ 479 => Some(EV_KEY::KEY_FN_2),
+ 480 => Some(EV_KEY::KEY_FN_D),
+ 481 => Some(EV_KEY::KEY_FN_E),
+ 482 => Some(EV_KEY::KEY_FN_F),
+ 483 => Some(EV_KEY::KEY_FN_S),
+ 484 => Some(EV_KEY::KEY_FN_B),
+ 485 => Some(EV_KEY::KEY_FN_RIGHT_SHIFT),
+ 497 => Some(EV_KEY::KEY_BRL_DOT1),
+ 498 => Some(EV_KEY::KEY_BRL_DOT2),
+ 499 => Some(EV_KEY::KEY_BRL_DOT3),
+ 500 => Some(EV_KEY::KEY_BRL_DOT4),
+ 501 => Some(EV_KEY::KEY_BRL_DOT5),
+ 502 => Some(EV_KEY::KEY_BRL_DOT6),
+ 503 => Some(EV_KEY::KEY_BRL_DOT7),
+ 504 => Some(EV_KEY::KEY_BRL_DOT8),
+ 505 => Some(EV_KEY::KEY_BRL_DOT9),
+ 506 => Some(EV_KEY::KEY_BRL_DOT10),
+ 512 => Some(EV_KEY::KEY_NUMERIC_0),
+ 513 => Some(EV_KEY::KEY_NUMERIC_1),
+ 514 => Some(EV_KEY::KEY_NUMERIC_2),
+ 515 => Some(EV_KEY::KEY_NUMERIC_3),
+ 516 => Some(EV_KEY::KEY_NUMERIC_4),
+ 517 => Some(EV_KEY::KEY_NUMERIC_5),
+ 518 => Some(EV_KEY::KEY_NUMERIC_6),
+ 519 => Some(EV_KEY::KEY_NUMERIC_7),
+ 520 => Some(EV_KEY::KEY_NUMERIC_8),
+ 521 => Some(EV_KEY::KEY_NUMERIC_9),
+ 522 => Some(EV_KEY::KEY_NUMERIC_STAR),
+ 523 => Some(EV_KEY::KEY_NUMERIC_POUND),
+ 524 => Some(EV_KEY::KEY_NUMERIC_A),
+ 525 => Some(EV_KEY::KEY_NUMERIC_B),
+ 526 => Some(EV_KEY::KEY_NUMERIC_C),
+ 527 => Some(EV_KEY::KEY_NUMERIC_D),
+ 528 => Some(EV_KEY::KEY_CAMERA_FOCUS),
+ 529 => Some(EV_KEY::KEY_WPS_BUTTON),
+ 530 => Some(EV_KEY::KEY_TOUCHPAD_TOGGLE),
+ 531 => Some(EV_KEY::KEY_TOUCHPAD_ON),
+ 532 => Some(EV_KEY::KEY_TOUCHPAD_OFF),
+ 533 => Some(EV_KEY::KEY_CAMERA_ZOOMIN),
+ 534 => Some(EV_KEY::KEY_CAMERA_ZOOMOUT),
+ 535 => Some(EV_KEY::KEY_CAMERA_UP),
+ 536 => Some(EV_KEY::KEY_CAMERA_DOWN),
+ 537 => Some(EV_KEY::KEY_CAMERA_LEFT),
+ 538 => Some(EV_KEY::KEY_CAMERA_RIGHT),
+ 539 => Some(EV_KEY::KEY_ATTENDANT_ON),
+ 540 => Some(EV_KEY::KEY_ATTENDANT_OFF),
+ 541 => Some(EV_KEY::KEY_ATTENDANT_TOGGLE),
+ 542 => Some(EV_KEY::KEY_LIGHTS_TOGGLE),
+ 560 => Some(EV_KEY::KEY_ALS_TOGGLE),
+ 561 => Some(EV_KEY::KEY_ROTATE_LOCK_TOGGLE),
+ 562 => Some(EV_KEY::KEY_REFRESH_RATE_TOGGLE),
+ 576 => Some(EV_KEY::KEY_BUTTONCONFIG),
+ 577 => Some(EV_KEY::KEY_TASKMANAGER),
+ 578 => Some(EV_KEY::KEY_JOURNAL),
+ 579 => Some(EV_KEY::KEY_CONTROLPANEL),
+ 580 => Some(EV_KEY::KEY_APPSELECT),
+ 581 => Some(EV_KEY::KEY_SCREENSAVER),
+ 582 => Some(EV_KEY::KEY_VOICECOMMAND),
+ 583 => Some(EV_KEY::KEY_ASSISTANT),
+ 584 => Some(EV_KEY::KEY_KBD_LAYOUT_NEXT),
+ 585 => Some(EV_KEY::KEY_EMOJI_PICKER),
+ 586 => Some(EV_KEY::KEY_DICTATE),
+ 587 => Some(EV_KEY::KEY_CAMERA_ACCESS_ENABLE),
+ 588 => Some(EV_KEY::KEY_CAMERA_ACCESS_DISABLE),
+ 589 => Some(EV_KEY::KEY_CAMERA_ACCESS_TOGGLE),
+ 590 => Some(EV_KEY::KEY_ACCESSIBILITY),
+ 591 => Some(EV_KEY::KEY_DO_NOT_DISTURB),
+ 592 => Some(EV_KEY::KEY_BRIGHTNESS_MIN),
+ 593 => Some(EV_KEY::KEY_BRIGHTNESS_MAX),
+ 608 => Some(EV_KEY::KEY_KBDINPUTASSIST_PREV),
+ 609 => Some(EV_KEY::KEY_KBDINPUTASSIST_NEXT),
+ 610 => Some(EV_KEY::KEY_KBDINPUTASSIST_PREVGROUP),
+ 611 => Some(EV_KEY::KEY_KBDINPUTASSIST_NEXTGROUP),
+ 612 => Some(EV_KEY::KEY_KBDINPUTASSIST_ACCEPT),
+ 613 => Some(EV_KEY::KEY_KBDINPUTASSIST_CANCEL),
+ 614 => Some(EV_KEY::KEY_RIGHT_UP),
+ 615 => Some(EV_KEY::KEY_RIGHT_DOWN),
+ 616 => Some(EV_KEY::KEY_LEFT_UP),
+ 617 => Some(EV_KEY::KEY_LEFT_DOWN),
+ 618 => Some(EV_KEY::KEY_ROOT_MENU),
+ 619 => Some(EV_KEY::KEY_MEDIA_TOP_MENU),
+ 620 => Some(EV_KEY::KEY_NUMERIC_11),
+ 621 => Some(EV_KEY::KEY_NUMERIC_12),
+ 622 => Some(EV_KEY::KEY_AUDIO_DESC),
+ 623 => Some(EV_KEY::KEY_3D_MODE),
+ 624 => Some(EV_KEY::KEY_NEXT_FAVORITE),
+ 625 => Some(EV_KEY::KEY_STOP_RECORD),
+ 626 => Some(EV_KEY::KEY_PAUSE_RECORD),
+ 627 => Some(EV_KEY::KEY_VOD),
+ 628 => Some(EV_KEY::KEY_UNMUTE),
+ 629 => Some(EV_KEY::KEY_FASTREVERSE),
+ 630 => Some(EV_KEY::KEY_SLOWREVERSE),
+ 631 => Some(EV_KEY::KEY_DATA),
+ 632 => Some(EV_KEY::KEY_ONSCREEN_KEYBOARD),
+ 633 => Some(EV_KEY::KEY_PRIVACY_SCREEN_TOGGLE),
+ 634 => Some(EV_KEY::KEY_SELECTIVE_SCREENSHOT),
+ 635 => Some(EV_KEY::KEY_NEXT_ELEMENT),
+ 636 => Some(EV_KEY::KEY_PREVIOUS_ELEMENT),
+ 637 => Some(EV_KEY::KEY_AUTOPILOT_ENGAGE_TOGGLE),
+ 638 => Some(EV_KEY::KEY_MARK_WAYPOINT),
+ 639 => Some(EV_KEY::KEY_SOS),
+ 640 => Some(EV_KEY::KEY_NAV_CHART),
+ 641 => Some(EV_KEY::KEY_FISHING_CHART),
+ 642 => Some(EV_KEY::KEY_SINGLE_RANGE_RADAR),
+ 643 => Some(EV_KEY::KEY_DUAL_RANGE_RADAR),
+ 644 => Some(EV_KEY::KEY_RADAR_OVERLAY),
+ 645 => Some(EV_KEY::KEY_TRADITIONAL_SONAR),
+ 646 => Some(EV_KEY::KEY_CLEARVU_SONAR),
+ 647 => Some(EV_KEY::KEY_SIDEVU_SONAR),
+ 648 => Some(EV_KEY::KEY_NAV_INFO),
+ 649 => Some(EV_KEY::KEY_BRIGHTNESS_MENU),
+ 656 => Some(EV_KEY::KEY_MACRO1),
+ 657 => Some(EV_KEY::KEY_MACRO2),
+ 658 => Some(EV_KEY::KEY_MACRO3),
+ 659 => Some(EV_KEY::KEY_MACRO4),
+ 660 => Some(EV_KEY::KEY_MACRO5),
+ 661 => Some(EV_KEY::KEY_MACRO6),
+ 662 => Some(EV_KEY::KEY_MACRO7),
+ 663 => Some(EV_KEY::KEY_MACRO8),
+ 664 => Some(EV_KEY::KEY_MACRO9),
+ 665 => Some(EV_KEY::KEY_MACRO10),
+ 666 => Some(EV_KEY::KEY_MACRO11),
+ 667 => Some(EV_KEY::KEY_MACRO12),
+ 668 => Some(EV_KEY::KEY_MACRO13),
+ 669 => Some(EV_KEY::KEY_MACRO14),
+ 670 => Some(EV_KEY::KEY_MACRO15),
+ 671 => Some(EV_KEY::KEY_MACRO16),
+ 672 => Some(EV_KEY::KEY_MACRO17),
+ 673 => Some(EV_KEY::KEY_MACRO18),
+ 674 => Some(EV_KEY::KEY_MACRO19),
+ 675 => Some(EV_KEY::KEY_MACRO20),
+ 676 => Some(EV_KEY::KEY_MACRO21),
+ 677 => Some(EV_KEY::KEY_MACRO22),
+ 678 => Some(EV_KEY::KEY_MACRO23),
+ 679 => Some(EV_KEY::KEY_MACRO24),
+ 680 => Some(EV_KEY::KEY_MACRO25),
+ 681 => Some(EV_KEY::KEY_MACRO26),
+ 682 => Some(EV_KEY::KEY_MACRO27),
+ 683 => Some(EV_KEY::KEY_MACRO28),
+ 684 => Some(EV_KEY::KEY_MACRO29),
+ 685 => Some(EV_KEY::KEY_MACRO30),
+ 688 => Some(EV_KEY::KEY_MACRO_RECORD_START),
+ 689 => Some(EV_KEY::KEY_MACRO_RECORD_STOP),
+ 690 => Some(EV_KEY::KEY_MACRO_PRESET_CYCLE),
+ 691 => Some(EV_KEY::KEY_MACRO_PRESET1),
+ 692 => Some(EV_KEY::KEY_MACRO_PRESET2),
+ 693 => Some(EV_KEY::KEY_MACRO_PRESET3),
+ 696 => Some(EV_KEY::KEY_KBD_LCD_MENU1),
+ 697 => Some(EV_KEY::KEY_KBD_LCD_MENU2),
+ 698 => Some(EV_KEY::KEY_KBD_LCD_MENU3),
+ 699 => Some(EV_KEY::KEY_KBD_LCD_MENU4),
+ 700 => Some(EV_KEY::KEY_KBD_LCD_MENU5),
+ 767 => Some(EV_KEY::KEY_MAX),
+ 256 => Some(EV_KEY::BTN_0),
+ 257 => Some(EV_KEY::BTN_1),
+ 258 => Some(EV_KEY::BTN_2),
+ 259 => Some(EV_KEY::BTN_3),
+ 260 => Some(EV_KEY::BTN_4),
+ 261 => Some(EV_KEY::BTN_5),
+ 262 => Some(EV_KEY::BTN_6),
+ 263 => Some(EV_KEY::BTN_7),
+ 264 => Some(EV_KEY::BTN_8),
+ 265 => Some(EV_KEY::BTN_9),
+ 272 => Some(EV_KEY::BTN_LEFT),
+ 273 => Some(EV_KEY::BTN_RIGHT),
+ 274 => Some(EV_KEY::BTN_MIDDLE),
+ 275 => Some(EV_KEY::BTN_SIDE),
+ 276 => Some(EV_KEY::BTN_EXTRA),
+ 277 => Some(EV_KEY::BTN_FORWARD),
+ 278 => Some(EV_KEY::BTN_BACK),
+ 279 => Some(EV_KEY::BTN_TASK),
+ 288 => Some(EV_KEY::BTN_TRIGGER),
+ 289 => Some(EV_KEY::BTN_THUMB),
+ 290 => Some(EV_KEY::BTN_THUMB2),
+ 291 => Some(EV_KEY::BTN_TOP),
+ 292 => Some(EV_KEY::BTN_TOP2),
+ 293 => Some(EV_KEY::BTN_PINKIE),
+ 294 => Some(EV_KEY::BTN_BASE),
+ 295 => Some(EV_KEY::BTN_BASE2),
+ 296 => Some(EV_KEY::BTN_BASE3),
+ 297 => Some(EV_KEY::BTN_BASE4),
+ 298 => Some(EV_KEY::BTN_BASE5),
+ 299 => Some(EV_KEY::BTN_BASE6),
+ 303 => Some(EV_KEY::BTN_DEAD),
+ 304 => Some(EV_KEY::BTN_SOUTH),
+ 305 => Some(EV_KEY::BTN_EAST),
+ 306 => Some(EV_KEY::BTN_C),
+ 307 => Some(EV_KEY::BTN_NORTH),
+ 308 => Some(EV_KEY::BTN_WEST),
+ 309 => Some(EV_KEY::BTN_Z),
+ 310 => Some(EV_KEY::BTN_TL),
+ 311 => Some(EV_KEY::BTN_TR),
+ 312 => Some(EV_KEY::BTN_TL2),
+ 313 => Some(EV_KEY::BTN_TR2),
+ 314 => Some(EV_KEY::BTN_SELECT),
+ 315 => Some(EV_KEY::BTN_START),
+ 316 => Some(EV_KEY::BTN_MODE),
+ 317 => Some(EV_KEY::BTN_THUMBL),
+ 318 => Some(EV_KEY::BTN_THUMBR),
+ 320 => Some(EV_KEY::BTN_TOOL_PEN),
+ 321 => Some(EV_KEY::BTN_TOOL_RUBBER),
+ 322 => Some(EV_KEY::BTN_TOOL_BRUSH),
+ 323 => Some(EV_KEY::BTN_TOOL_PENCIL),
+ 324 => Some(EV_KEY::BTN_TOOL_AIRBRUSH),
+ 325 => Some(EV_KEY::BTN_TOOL_FINGER),
+ 326 => Some(EV_KEY::BTN_TOOL_MOUSE),
+ 327 => Some(EV_KEY::BTN_TOOL_LENS),
+ 328 => Some(EV_KEY::BTN_TOOL_QUINTTAP),
+ 329 => Some(EV_KEY::BTN_STYLUS3),
+ 330 => Some(EV_KEY::BTN_TOUCH),
+ 331 => Some(EV_KEY::BTN_STYLUS),
+ 332 => Some(EV_KEY::BTN_STYLUS2),
+ 333 => Some(EV_KEY::BTN_TOOL_DOUBLETAP),
+ 334 => Some(EV_KEY::BTN_TOOL_TRIPLETAP),
+ 335 => Some(EV_KEY::BTN_TOOL_QUADTAP),
+ 336 => Some(EV_KEY::BTN_GEAR_DOWN),
+ 337 => Some(EV_KEY::BTN_GEAR_UP),
+ 544 => Some(EV_KEY::BTN_DPAD_UP),
+ 545 => Some(EV_KEY::BTN_DPAD_DOWN),
+ 546 => Some(EV_KEY::BTN_DPAD_LEFT),
+ 547 => Some(EV_KEY::BTN_DPAD_RIGHT),
+ 704 => Some(EV_KEY::BTN_TRIGGER_HAPPY1),
+ 705 => Some(EV_KEY::BTN_TRIGGER_HAPPY2),
+ 706 => Some(EV_KEY::BTN_TRIGGER_HAPPY3),
+ 707 => Some(EV_KEY::BTN_TRIGGER_HAPPY4),
+ 708 => Some(EV_KEY::BTN_TRIGGER_HAPPY5),
+ 709 => Some(EV_KEY::BTN_TRIGGER_HAPPY6),
+ 710 => Some(EV_KEY::BTN_TRIGGER_HAPPY7),
+ 711 => Some(EV_KEY::BTN_TRIGGER_HAPPY8),
+ 712 => Some(EV_KEY::BTN_TRIGGER_HAPPY9),
+ 713 => Some(EV_KEY::BTN_TRIGGER_HAPPY10),
+ 714 => Some(EV_KEY::BTN_TRIGGER_HAPPY11),
+ 715 => Some(EV_KEY::BTN_TRIGGER_HAPPY12),
+ 716 => Some(EV_KEY::BTN_TRIGGER_HAPPY13),
+ 717 => Some(EV_KEY::BTN_TRIGGER_HAPPY14),
+ 718 => Some(EV_KEY::BTN_TRIGGER_HAPPY15),
+ 719 => Some(EV_KEY::BTN_TRIGGER_HAPPY16),
+ 720 => Some(EV_KEY::BTN_TRIGGER_HAPPY17),
+ 721 => Some(EV_KEY::BTN_TRIGGER_HAPPY18),
+ 722 => Some(EV_KEY::BTN_TRIGGER_HAPPY19),
+ 723 => Some(EV_KEY::BTN_TRIGGER_HAPPY20),
+ 724 => Some(EV_KEY::BTN_TRIGGER_HAPPY21),
+ 725 => Some(EV_KEY::BTN_TRIGGER_HAPPY22),
+ 726 => Some(EV_KEY::BTN_TRIGGER_HAPPY23),
+ 727 => Some(EV_KEY::BTN_TRIGGER_HAPPY24),
+ 728 => Some(EV_KEY::BTN_TRIGGER_HAPPY25),
+ 729 => Some(EV_KEY::BTN_TRIGGER_HAPPY26),
+ 730 => Some(EV_KEY::BTN_TRIGGER_HAPPY27),
+ 731 => Some(EV_KEY::BTN_TRIGGER_HAPPY28),
+ 732 => Some(EV_KEY::BTN_TRIGGER_HAPPY29),
+ 733 => Some(EV_KEY::BTN_TRIGGER_HAPPY30),
+ 734 => Some(EV_KEY::BTN_TRIGGER_HAPPY31),
+ 735 => Some(EV_KEY::BTN_TRIGGER_HAPPY32),
+ 736 => Some(EV_KEY::BTN_TRIGGER_HAPPY33),
+ 737 => Some(EV_KEY::BTN_TRIGGER_HAPPY34),
+ 738 => Some(EV_KEY::BTN_TRIGGER_HAPPY35),
+ 739 => Some(EV_KEY::BTN_TRIGGER_HAPPY36),
+ 740 => Some(EV_KEY::BTN_TRIGGER_HAPPY37),
+ 741 => Some(EV_KEY::BTN_TRIGGER_HAPPY38),
+ 742 => Some(EV_KEY::BTN_TRIGGER_HAPPY39),
+ 743 => Some(EV_KEY::BTN_TRIGGER_HAPPY40),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_KEY {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "KEY_RESERVED" => Ok(EV_KEY::KEY_RESERVED),
+ "KEY_ESC" => Ok(EV_KEY::KEY_ESC),
+ "KEY_1" => Ok(EV_KEY::KEY_1),
+ "KEY_2" => Ok(EV_KEY::KEY_2),
+ "KEY_3" => Ok(EV_KEY::KEY_3),
+ "KEY_4" => Ok(EV_KEY::KEY_4),
+ "KEY_5" => Ok(EV_KEY::KEY_5),
+ "KEY_6" => Ok(EV_KEY::KEY_6),
+ "KEY_7" => Ok(EV_KEY::KEY_7),
+ "KEY_8" => Ok(EV_KEY::KEY_8),
+ "KEY_9" => Ok(EV_KEY::KEY_9),
+ "KEY_0" => Ok(EV_KEY::KEY_0),
+ "KEY_MINUS" => Ok(EV_KEY::KEY_MINUS),
+ "KEY_EQUAL" => Ok(EV_KEY::KEY_EQUAL),
+ "KEY_BACKSPACE" => Ok(EV_KEY::KEY_BACKSPACE),
+ "KEY_TAB" => Ok(EV_KEY::KEY_TAB),
+ "KEY_Q" => Ok(EV_KEY::KEY_Q),
+ "KEY_W" => Ok(EV_KEY::KEY_W),
+ "KEY_E" => Ok(EV_KEY::KEY_E),
+ "KEY_R" => Ok(EV_KEY::KEY_R),
+ "KEY_T" => Ok(EV_KEY::KEY_T),
+ "KEY_Y" => Ok(EV_KEY::KEY_Y),
+ "KEY_U" => Ok(EV_KEY::KEY_U),
+ "KEY_I" => Ok(EV_KEY::KEY_I),
+ "KEY_O" => Ok(EV_KEY::KEY_O),
+ "KEY_P" => Ok(EV_KEY::KEY_P),
+ "KEY_LEFTBRACE" => Ok(EV_KEY::KEY_LEFTBRACE),
+ "KEY_RIGHTBRACE" => Ok(EV_KEY::KEY_RIGHTBRACE),
+ "KEY_ENTER" => Ok(EV_KEY::KEY_ENTER),
+ "KEY_LEFTCTRL" => Ok(EV_KEY::KEY_LEFTCTRL),
+ "KEY_A" => Ok(EV_KEY::KEY_A),
+ "KEY_S" => Ok(EV_KEY::KEY_S),
+ "KEY_D" => Ok(EV_KEY::KEY_D),
+ "KEY_F" => Ok(EV_KEY::KEY_F),
+ "KEY_G" => Ok(EV_KEY::KEY_G),
+ "KEY_H" => Ok(EV_KEY::KEY_H),
+ "KEY_J" => Ok(EV_KEY::KEY_J),
+ "KEY_K" => Ok(EV_KEY::KEY_K),
+ "KEY_L" => Ok(EV_KEY::KEY_L),
+ "KEY_SEMICOLON" => Ok(EV_KEY::KEY_SEMICOLON),
+ "KEY_APOSTROPHE" => Ok(EV_KEY::KEY_APOSTROPHE),
+ "KEY_GRAVE" => Ok(EV_KEY::KEY_GRAVE),
+ "KEY_LEFTSHIFT" => Ok(EV_KEY::KEY_LEFTSHIFT),
+ "KEY_BACKSLASH" => Ok(EV_KEY::KEY_BACKSLASH),
+ "KEY_Z" => Ok(EV_KEY::KEY_Z),
+ "KEY_X" => Ok(EV_KEY::KEY_X),
+ "KEY_C" => Ok(EV_KEY::KEY_C),
+ "KEY_V" => Ok(EV_KEY::KEY_V),
+ "KEY_B" => Ok(EV_KEY::KEY_B),
+ "KEY_N" => Ok(EV_KEY::KEY_N),
+ "KEY_M" => Ok(EV_KEY::KEY_M),
+ "KEY_COMMA" => Ok(EV_KEY::KEY_COMMA),
+ "KEY_DOT" => Ok(EV_KEY::KEY_DOT),
+ "KEY_SLASH" => Ok(EV_KEY::KEY_SLASH),
+ "KEY_RIGHTSHIFT" => Ok(EV_KEY::KEY_RIGHTSHIFT),
+ "KEY_KPASTERISK" => Ok(EV_KEY::KEY_KPASTERISK),
+ "KEY_LEFTALT" => Ok(EV_KEY::KEY_LEFTALT),
+ "KEY_SPACE" => Ok(EV_KEY::KEY_SPACE),
+ "KEY_CAPSLOCK" => Ok(EV_KEY::KEY_CAPSLOCK),
+ "KEY_F1" => Ok(EV_KEY::KEY_F1),
+ "KEY_F2" => Ok(EV_KEY::KEY_F2),
+ "KEY_F3" => Ok(EV_KEY::KEY_F3),
+ "KEY_F4" => Ok(EV_KEY::KEY_F4),
+ "KEY_F5" => Ok(EV_KEY::KEY_F5),
+ "KEY_F6" => Ok(EV_KEY::KEY_F6),
+ "KEY_F7" => Ok(EV_KEY::KEY_F7),
+ "KEY_F8" => Ok(EV_KEY::KEY_F8),
+ "KEY_F9" => Ok(EV_KEY::KEY_F9),
+ "KEY_F10" => Ok(EV_KEY::KEY_F10),
+ "KEY_NUMLOCK" => Ok(EV_KEY::KEY_NUMLOCK),
+ "KEY_SCROLLLOCK" => Ok(EV_KEY::KEY_SCROLLLOCK),
+ "KEY_KP7" => Ok(EV_KEY::KEY_KP7),
+ "KEY_KP8" => Ok(EV_KEY::KEY_KP8),
+ "KEY_KP9" => Ok(EV_KEY::KEY_KP9),
+ "KEY_KPMINUS" => Ok(EV_KEY::KEY_KPMINUS),
+ "KEY_KP4" => Ok(EV_KEY::KEY_KP4),
+ "KEY_KP5" => Ok(EV_KEY::KEY_KP5),
+ "KEY_KP6" => Ok(EV_KEY::KEY_KP6),
+ "KEY_KPPLUS" => Ok(EV_KEY::KEY_KPPLUS),
+ "KEY_KP1" => Ok(EV_KEY::KEY_KP1),
+ "KEY_KP2" => Ok(EV_KEY::KEY_KP2),
+ "KEY_KP3" => Ok(EV_KEY::KEY_KP3),
+ "KEY_KP0" => Ok(EV_KEY::KEY_KP0),
+ "KEY_KPDOT" => Ok(EV_KEY::KEY_KPDOT),
+ "KEY_ZENKAKUHANKAKU" => Ok(EV_KEY::KEY_ZENKAKUHANKAKU),
+ "KEY_102ND" => Ok(EV_KEY::KEY_102ND),
+ "KEY_F11" => Ok(EV_KEY::KEY_F11),
+ "KEY_F12" => Ok(EV_KEY::KEY_F12),
+ "KEY_RO" => Ok(EV_KEY::KEY_RO),
+ "KEY_KATAKANA" => Ok(EV_KEY::KEY_KATAKANA),
+ "KEY_HIRAGANA" => Ok(EV_KEY::KEY_HIRAGANA),
+ "KEY_HENKAN" => Ok(EV_KEY::KEY_HENKAN),
+ "KEY_KATAKANAHIRAGANA" => Ok(EV_KEY::KEY_KATAKANAHIRAGANA),
+ "KEY_MUHENKAN" => Ok(EV_KEY::KEY_MUHENKAN),
+ "KEY_KPJPCOMMA" => Ok(EV_KEY::KEY_KPJPCOMMA),
+ "KEY_KPENTER" => Ok(EV_KEY::KEY_KPENTER),
+ "KEY_RIGHTCTRL" => Ok(EV_KEY::KEY_RIGHTCTRL),
+ "KEY_KPSLASH" => Ok(EV_KEY::KEY_KPSLASH),
+ "KEY_SYSRQ" => Ok(EV_KEY::KEY_SYSRQ),
+ "KEY_RIGHTALT" => Ok(EV_KEY::KEY_RIGHTALT),
+ "KEY_LINEFEED" => Ok(EV_KEY::KEY_LINEFEED),
+ "KEY_HOME" => Ok(EV_KEY::KEY_HOME),
+ "KEY_UP" => Ok(EV_KEY::KEY_UP),
+ "KEY_PAGEUP" => Ok(EV_KEY::KEY_PAGEUP),
+ "KEY_LEFT" => Ok(EV_KEY::KEY_LEFT),
+ "KEY_RIGHT" => Ok(EV_KEY::KEY_RIGHT),
+ "KEY_END" => Ok(EV_KEY::KEY_END),
+ "KEY_DOWN" => Ok(EV_KEY::KEY_DOWN),
+ "KEY_PAGEDOWN" => Ok(EV_KEY::KEY_PAGEDOWN),
+ "KEY_INSERT" => Ok(EV_KEY::KEY_INSERT),
+ "KEY_DELETE" => Ok(EV_KEY::KEY_DELETE),
+ "KEY_MACRO" => Ok(EV_KEY::KEY_MACRO),
+ "KEY_MUTE" => Ok(EV_KEY::KEY_MUTE),
+ "KEY_VOLUMEDOWN" => Ok(EV_KEY::KEY_VOLUMEDOWN),
+ "KEY_VOLUMEUP" => Ok(EV_KEY::KEY_VOLUMEUP),
+ "KEY_POWER" => Ok(EV_KEY::KEY_POWER),
+ "KEY_KPEQUAL" => Ok(EV_KEY::KEY_KPEQUAL),
+ "KEY_KPPLUSMINUS" => Ok(EV_KEY::KEY_KPPLUSMINUS),
+ "KEY_PAUSE" => Ok(EV_KEY::KEY_PAUSE),
+ "KEY_SCALE" => Ok(EV_KEY::KEY_SCALE),
+ "KEY_KPCOMMA" => Ok(EV_KEY::KEY_KPCOMMA),
+ "KEY_HANGEUL" => Ok(EV_KEY::KEY_HANGEUL),
+ "KEY_HANJA" => Ok(EV_KEY::KEY_HANJA),
+ "KEY_YEN" => Ok(EV_KEY::KEY_YEN),
+ "KEY_LEFTMETA" => Ok(EV_KEY::KEY_LEFTMETA),
+ "KEY_RIGHTMETA" => Ok(EV_KEY::KEY_RIGHTMETA),
+ "KEY_COMPOSE" => Ok(EV_KEY::KEY_COMPOSE),
+ "KEY_STOP" => Ok(EV_KEY::KEY_STOP),
+ "KEY_AGAIN" => Ok(EV_KEY::KEY_AGAIN),
+ "KEY_PROPS" => Ok(EV_KEY::KEY_PROPS),
+ "KEY_UNDO" => Ok(EV_KEY::KEY_UNDO),
+ "KEY_FRONT" => Ok(EV_KEY::KEY_FRONT),
+ "KEY_COPY" => Ok(EV_KEY::KEY_COPY),
+ "KEY_OPEN" => Ok(EV_KEY::KEY_OPEN),
+ "KEY_PASTE" => Ok(EV_KEY::KEY_PASTE),
+ "KEY_FIND" => Ok(EV_KEY::KEY_FIND),
+ "KEY_CUT" => Ok(EV_KEY::KEY_CUT),
+ "KEY_HELP" => Ok(EV_KEY::KEY_HELP),
+ "KEY_MENU" => Ok(EV_KEY::KEY_MENU),
+ "KEY_CALC" => Ok(EV_KEY::KEY_CALC),
+ "KEY_SETUP" => Ok(EV_KEY::KEY_SETUP),
+ "KEY_SLEEP" => Ok(EV_KEY::KEY_SLEEP),
+ "KEY_WAKEUP" => Ok(EV_KEY::KEY_WAKEUP),
+ "KEY_FILE" => Ok(EV_KEY::KEY_FILE),
+ "KEY_SENDFILE" => Ok(EV_KEY::KEY_SENDFILE),
+ "KEY_DELETEFILE" => Ok(EV_KEY::KEY_DELETEFILE),
+ "KEY_XFER" => Ok(EV_KEY::KEY_XFER),
+ "KEY_PROG1" => Ok(EV_KEY::KEY_PROG1),
+ "KEY_PROG2" => Ok(EV_KEY::KEY_PROG2),
+ "KEY_WWW" => Ok(EV_KEY::KEY_WWW),
+ "KEY_MSDOS" => Ok(EV_KEY::KEY_MSDOS),
+ "KEY_COFFEE" => Ok(EV_KEY::KEY_COFFEE),
+ "KEY_ROTATE_DISPLAY" => Ok(EV_KEY::KEY_ROTATE_DISPLAY),
+ "KEY_CYCLEWINDOWS" => Ok(EV_KEY::KEY_CYCLEWINDOWS),
+ "KEY_MAIL" => Ok(EV_KEY::KEY_MAIL),
+ "KEY_BOOKMARKS" => Ok(EV_KEY::KEY_BOOKMARKS),
+ "KEY_COMPUTER" => Ok(EV_KEY::KEY_COMPUTER),
+ "KEY_BACK" => Ok(EV_KEY::KEY_BACK),
+ "KEY_FORWARD" => Ok(EV_KEY::KEY_FORWARD),
+ "KEY_CLOSECD" => Ok(EV_KEY::KEY_CLOSECD),
+ "KEY_EJECTCD" => Ok(EV_KEY::KEY_EJECTCD),
+ "KEY_EJECTCLOSECD" => Ok(EV_KEY::KEY_EJECTCLOSECD),
+ "KEY_NEXTSONG" => Ok(EV_KEY::KEY_NEXTSONG),
+ "KEY_PLAYPAUSE" => Ok(EV_KEY::KEY_PLAYPAUSE),
+ "KEY_PREVIOUSSONG" => Ok(EV_KEY::KEY_PREVIOUSSONG),
+ "KEY_STOPCD" => Ok(EV_KEY::KEY_STOPCD),
+ "KEY_RECORD" => Ok(EV_KEY::KEY_RECORD),
+ "KEY_REWIND" => Ok(EV_KEY::KEY_REWIND),
+ "KEY_PHONE" => Ok(EV_KEY::KEY_PHONE),
+ "KEY_ISO" => Ok(EV_KEY::KEY_ISO),
+ "KEY_CONFIG" => Ok(EV_KEY::KEY_CONFIG),
+ "KEY_HOMEPAGE" => Ok(EV_KEY::KEY_HOMEPAGE),
+ "KEY_REFRESH" => Ok(EV_KEY::KEY_REFRESH),
+ "KEY_EXIT" => Ok(EV_KEY::KEY_EXIT),
+ "KEY_MOVE" => Ok(EV_KEY::KEY_MOVE),
+ "KEY_EDIT" => Ok(EV_KEY::KEY_EDIT),
+ "KEY_SCROLLUP" => Ok(EV_KEY::KEY_SCROLLUP),
+ "KEY_SCROLLDOWN" => Ok(EV_KEY::KEY_SCROLLDOWN),
+ "KEY_KPLEFTPAREN" => Ok(EV_KEY::KEY_KPLEFTPAREN),
+ "KEY_KPRIGHTPAREN" => Ok(EV_KEY::KEY_KPRIGHTPAREN),
+ "KEY_NEW" => Ok(EV_KEY::KEY_NEW),
+ "KEY_REDO" => Ok(EV_KEY::KEY_REDO),
+ "KEY_F13" => Ok(EV_KEY::KEY_F13),
+ "KEY_F14" => Ok(EV_KEY::KEY_F14),
+ "KEY_F15" => Ok(EV_KEY::KEY_F15),
+ "KEY_F16" => Ok(EV_KEY::KEY_F16),
+ "KEY_F17" => Ok(EV_KEY::KEY_F17),
+ "KEY_F18" => Ok(EV_KEY::KEY_F18),
+ "KEY_F19" => Ok(EV_KEY::KEY_F19),
+ "KEY_F20" => Ok(EV_KEY::KEY_F20),
+ "KEY_F21" => Ok(EV_KEY::KEY_F21),
+ "KEY_F22" => Ok(EV_KEY::KEY_F22),
+ "KEY_F23" => Ok(EV_KEY::KEY_F23),
+ "KEY_F24" => Ok(EV_KEY::KEY_F24),
+ "KEY_PLAYCD" => Ok(EV_KEY::KEY_PLAYCD),
+ "KEY_PAUSECD" => Ok(EV_KEY::KEY_PAUSECD),
+ "KEY_PROG3" => Ok(EV_KEY::KEY_PROG3),
+ "KEY_PROG4" => Ok(EV_KEY::KEY_PROG4),
+ "KEY_ALL_APPLICATIONS" => Ok(EV_KEY::KEY_ALL_APPLICATIONS),
+ "KEY_SUSPEND" => Ok(EV_KEY::KEY_SUSPEND),
+ "KEY_CLOSE" => Ok(EV_KEY::KEY_CLOSE),
+ "KEY_PLAY" => Ok(EV_KEY::KEY_PLAY),
+ "KEY_FASTFORWARD" => Ok(EV_KEY::KEY_FASTFORWARD),
+ "KEY_BASSBOOST" => Ok(EV_KEY::KEY_BASSBOOST),
+ "KEY_PRINT" => Ok(EV_KEY::KEY_PRINT),
+ "KEY_HP" => Ok(EV_KEY::KEY_HP),
+ "KEY_CAMERA" => Ok(EV_KEY::KEY_CAMERA),
+ "KEY_SOUND" => Ok(EV_KEY::KEY_SOUND),
+ "KEY_QUESTION" => Ok(EV_KEY::KEY_QUESTION),
+ "KEY_EMAIL" => Ok(EV_KEY::KEY_EMAIL),
+ "KEY_CHAT" => Ok(EV_KEY::KEY_CHAT),
+ "KEY_SEARCH" => Ok(EV_KEY::KEY_SEARCH),
+ "KEY_CONNECT" => Ok(EV_KEY::KEY_CONNECT),
+ "KEY_FINANCE" => Ok(EV_KEY::KEY_FINANCE),
+ "KEY_SPORT" => Ok(EV_KEY::KEY_SPORT),
+ "KEY_SHOP" => Ok(EV_KEY::KEY_SHOP),
+ "KEY_ALTERASE" => Ok(EV_KEY::KEY_ALTERASE),
+ "KEY_CANCEL" => Ok(EV_KEY::KEY_CANCEL),
+ "KEY_BRIGHTNESSDOWN" => Ok(EV_KEY::KEY_BRIGHTNESSDOWN),
+ "KEY_BRIGHTNESSUP" => Ok(EV_KEY::KEY_BRIGHTNESSUP),
+ "KEY_MEDIA" => Ok(EV_KEY::KEY_MEDIA),
+ "KEY_SWITCHVIDEOMODE" => Ok(EV_KEY::KEY_SWITCHVIDEOMODE),
+ "KEY_KBDILLUMTOGGLE" => Ok(EV_KEY::KEY_KBDILLUMTOGGLE),
+ "KEY_KBDILLUMDOWN" => Ok(EV_KEY::KEY_KBDILLUMDOWN),
+ "KEY_KBDILLUMUP" => Ok(EV_KEY::KEY_KBDILLUMUP),
+ "KEY_SEND" => Ok(EV_KEY::KEY_SEND),
+ "KEY_REPLY" => Ok(EV_KEY::KEY_REPLY),
+ "KEY_FORWARDMAIL" => Ok(EV_KEY::KEY_FORWARDMAIL),
+ "KEY_SAVE" => Ok(EV_KEY::KEY_SAVE),
+ "KEY_DOCUMENTS" => Ok(EV_KEY::KEY_DOCUMENTS),
+ "KEY_BATTERY" => Ok(EV_KEY::KEY_BATTERY),
+ "KEY_BLUETOOTH" => Ok(EV_KEY::KEY_BLUETOOTH),
+ "KEY_WLAN" => Ok(EV_KEY::KEY_WLAN),
+ "KEY_UWB" => Ok(EV_KEY::KEY_UWB),
+ "KEY_UNKNOWN" => Ok(EV_KEY::KEY_UNKNOWN),
+ "KEY_VIDEO_NEXT" => Ok(EV_KEY::KEY_VIDEO_NEXT),
+ "KEY_VIDEO_PREV" => Ok(EV_KEY::KEY_VIDEO_PREV),
+ "KEY_BRIGHTNESS_CYCLE" => Ok(EV_KEY::KEY_BRIGHTNESS_CYCLE),
+ "KEY_BRIGHTNESS_AUTO" => Ok(EV_KEY::KEY_BRIGHTNESS_AUTO),
+ "KEY_DISPLAY_OFF" => Ok(EV_KEY::KEY_DISPLAY_OFF),
+ "KEY_WWAN" => Ok(EV_KEY::KEY_WWAN),
+ "KEY_RFKILL" => Ok(EV_KEY::KEY_RFKILL),
+ "KEY_MICMUTE" => Ok(EV_KEY::KEY_MICMUTE),
+ "KEY_OK" => Ok(EV_KEY::KEY_OK),
+ "KEY_SELECT" => Ok(EV_KEY::KEY_SELECT),
+ "KEY_GOTO" => Ok(EV_KEY::KEY_GOTO),
+ "KEY_CLEAR" => Ok(EV_KEY::KEY_CLEAR),
+ "KEY_POWER2" => Ok(EV_KEY::KEY_POWER2),
+ "KEY_OPTION" => Ok(EV_KEY::KEY_OPTION),
+ "KEY_INFO" => Ok(EV_KEY::KEY_INFO),
+ "KEY_TIME" => Ok(EV_KEY::KEY_TIME),
+ "KEY_VENDOR" => Ok(EV_KEY::KEY_VENDOR),
+ "KEY_ARCHIVE" => Ok(EV_KEY::KEY_ARCHIVE),
+ "KEY_PROGRAM" => Ok(EV_KEY::KEY_PROGRAM),
+ "KEY_CHANNEL" => Ok(EV_KEY::KEY_CHANNEL),
+ "KEY_FAVORITES" => Ok(EV_KEY::KEY_FAVORITES),
+ "KEY_EPG" => Ok(EV_KEY::KEY_EPG),
+ "KEY_PVR" => Ok(EV_KEY::KEY_PVR),
+ "KEY_MHP" => Ok(EV_KEY::KEY_MHP),
+ "KEY_LANGUAGE" => Ok(EV_KEY::KEY_LANGUAGE),
+ "KEY_TITLE" => Ok(EV_KEY::KEY_TITLE),
+ "KEY_SUBTITLE" => Ok(EV_KEY::KEY_SUBTITLE),
+ "KEY_ANGLE" => Ok(EV_KEY::KEY_ANGLE),
+ "KEY_FULL_SCREEN" => Ok(EV_KEY::KEY_FULL_SCREEN),
+ "KEY_MODE" => Ok(EV_KEY::KEY_MODE),
+ "KEY_KEYBOARD" => Ok(EV_KEY::KEY_KEYBOARD),
+ "KEY_ASPECT_RATIO" => Ok(EV_KEY::KEY_ASPECT_RATIO),
+ "KEY_PC" => Ok(EV_KEY::KEY_PC),
+ "KEY_TV" => Ok(EV_KEY::KEY_TV),
+ "KEY_TV2" => Ok(EV_KEY::KEY_TV2),
+ "KEY_VCR" => Ok(EV_KEY::KEY_VCR),
+ "KEY_VCR2" => Ok(EV_KEY::KEY_VCR2),
+ "KEY_SAT" => Ok(EV_KEY::KEY_SAT),
+ "KEY_SAT2" => Ok(EV_KEY::KEY_SAT2),
+ "KEY_CD" => Ok(EV_KEY::KEY_CD),
+ "KEY_TAPE" => Ok(EV_KEY::KEY_TAPE),
+ "KEY_RADIO" => Ok(EV_KEY::KEY_RADIO),
+ "KEY_TUNER" => Ok(EV_KEY::KEY_TUNER),
+ "KEY_PLAYER" => Ok(EV_KEY::KEY_PLAYER),
+ "KEY_TEXT" => Ok(EV_KEY::KEY_TEXT),
+ "KEY_DVD" => Ok(EV_KEY::KEY_DVD),
+ "KEY_AUX" => Ok(EV_KEY::KEY_AUX),
+ "KEY_MP3" => Ok(EV_KEY::KEY_MP3),
+ "KEY_AUDIO" => Ok(EV_KEY::KEY_AUDIO),
+ "KEY_VIDEO" => Ok(EV_KEY::KEY_VIDEO),
+ "KEY_DIRECTORY" => Ok(EV_KEY::KEY_DIRECTORY),
+ "KEY_LIST" => Ok(EV_KEY::KEY_LIST),
+ "KEY_MEMO" => Ok(EV_KEY::KEY_MEMO),
+ "KEY_CALENDAR" => Ok(EV_KEY::KEY_CALENDAR),
+ "KEY_RED" => Ok(EV_KEY::KEY_RED),
+ "KEY_GREEN" => Ok(EV_KEY::KEY_GREEN),
+ "KEY_YELLOW" => Ok(EV_KEY::KEY_YELLOW),
+ "KEY_BLUE" => Ok(EV_KEY::KEY_BLUE),
+ "KEY_CHANNELUP" => Ok(EV_KEY::KEY_CHANNELUP),
+ "KEY_CHANNELDOWN" => Ok(EV_KEY::KEY_CHANNELDOWN),
+ "KEY_FIRST" => Ok(EV_KEY::KEY_FIRST),
+ "KEY_LAST" => Ok(EV_KEY::KEY_LAST),
+ "KEY_AB" => Ok(EV_KEY::KEY_AB),
+ "KEY_NEXT" => Ok(EV_KEY::KEY_NEXT),
+ "KEY_RESTART" => Ok(EV_KEY::KEY_RESTART),
+ "KEY_SLOW" => Ok(EV_KEY::KEY_SLOW),
+ "KEY_SHUFFLE" => Ok(EV_KEY::KEY_SHUFFLE),
+ "KEY_BREAK" => Ok(EV_KEY::KEY_BREAK),
+ "KEY_PREVIOUS" => Ok(EV_KEY::KEY_PREVIOUS),
+ "KEY_DIGITS" => Ok(EV_KEY::KEY_DIGITS),
+ "KEY_TEEN" => Ok(EV_KEY::KEY_TEEN),
+ "KEY_TWEN" => Ok(EV_KEY::KEY_TWEN),
+ "KEY_VIDEOPHONE" => Ok(EV_KEY::KEY_VIDEOPHONE),
+ "KEY_GAMES" => Ok(EV_KEY::KEY_GAMES),
+ "KEY_ZOOMIN" => Ok(EV_KEY::KEY_ZOOMIN),
+ "KEY_ZOOMOUT" => Ok(EV_KEY::KEY_ZOOMOUT),
+ "KEY_ZOOMRESET" => Ok(EV_KEY::KEY_ZOOMRESET),
+ "KEY_WORDPROCESSOR" => Ok(EV_KEY::KEY_WORDPROCESSOR),
+ "KEY_EDITOR" => Ok(EV_KEY::KEY_EDITOR),
+ "KEY_SPREADSHEET" => Ok(EV_KEY::KEY_SPREADSHEET),
+ "KEY_GRAPHICSEDITOR" => Ok(EV_KEY::KEY_GRAPHICSEDITOR),
+ "KEY_PRESENTATION" => Ok(EV_KEY::KEY_PRESENTATION),
+ "KEY_DATABASE" => Ok(EV_KEY::KEY_DATABASE),
+ "KEY_NEWS" => Ok(EV_KEY::KEY_NEWS),
+ "KEY_VOICEMAIL" => Ok(EV_KEY::KEY_VOICEMAIL),
+ "KEY_ADDRESSBOOK" => Ok(EV_KEY::KEY_ADDRESSBOOK),
+ "KEY_MESSENGER" => Ok(EV_KEY::KEY_MESSENGER),
+ "KEY_DISPLAYTOGGLE" => Ok(EV_KEY::KEY_DISPLAYTOGGLE),
+ "KEY_SPELLCHECK" => Ok(EV_KEY::KEY_SPELLCHECK),
+ "KEY_LOGOFF" => Ok(EV_KEY::KEY_LOGOFF),
+ "KEY_DOLLAR" => Ok(EV_KEY::KEY_DOLLAR),
+ "KEY_EURO" => Ok(EV_KEY::KEY_EURO),
+ "KEY_FRAMEBACK" => Ok(EV_KEY::KEY_FRAMEBACK),
+ "KEY_FRAMEFORWARD" => Ok(EV_KEY::KEY_FRAMEFORWARD),
+ "KEY_CONTEXT_MENU" => Ok(EV_KEY::KEY_CONTEXT_MENU),
+ "KEY_MEDIA_REPEAT" => Ok(EV_KEY::KEY_MEDIA_REPEAT),
+ "KEY_10CHANNELSUP" => Ok(EV_KEY::KEY_10CHANNELSUP),
+ "KEY_10CHANNELSDOWN" => Ok(EV_KEY::KEY_10CHANNELSDOWN),
+ "KEY_IMAGES" => Ok(EV_KEY::KEY_IMAGES),
+ "KEY_NOTIFICATION_CENTER" => Ok(EV_KEY::KEY_NOTIFICATION_CENTER),
+ "KEY_PICKUP_PHONE" => Ok(EV_KEY::KEY_PICKUP_PHONE),
+ "KEY_HANGUP_PHONE" => Ok(EV_KEY::KEY_HANGUP_PHONE),
+ "KEY_DEL_EOL" => Ok(EV_KEY::KEY_DEL_EOL),
+ "KEY_DEL_EOS" => Ok(EV_KEY::KEY_DEL_EOS),
+ "KEY_INS_LINE" => Ok(EV_KEY::KEY_INS_LINE),
+ "KEY_DEL_LINE" => Ok(EV_KEY::KEY_DEL_LINE),
+ "KEY_FN" => Ok(EV_KEY::KEY_FN),
+ "KEY_FN_ESC" => Ok(EV_KEY::KEY_FN_ESC),
+ "KEY_FN_F1" => Ok(EV_KEY::KEY_FN_F1),
+ "KEY_FN_F2" => Ok(EV_KEY::KEY_FN_F2),
+ "KEY_FN_F3" => Ok(EV_KEY::KEY_FN_F3),
+ "KEY_FN_F4" => Ok(EV_KEY::KEY_FN_F4),
+ "KEY_FN_F5" => Ok(EV_KEY::KEY_FN_F5),
+ "KEY_FN_F6" => Ok(EV_KEY::KEY_FN_F6),
+ "KEY_FN_F7" => Ok(EV_KEY::KEY_FN_F7),
+ "KEY_FN_F8" => Ok(EV_KEY::KEY_FN_F8),
+ "KEY_FN_F9" => Ok(EV_KEY::KEY_FN_F9),
+ "KEY_FN_F10" => Ok(EV_KEY::KEY_FN_F10),
+ "KEY_FN_F11" => Ok(EV_KEY::KEY_FN_F11),
+ "KEY_FN_F12" => Ok(EV_KEY::KEY_FN_F12),
+ "KEY_FN_1" => Ok(EV_KEY::KEY_FN_1),
+ "KEY_FN_2" => Ok(EV_KEY::KEY_FN_2),
+ "KEY_FN_D" => Ok(EV_KEY::KEY_FN_D),
+ "KEY_FN_E" => Ok(EV_KEY::KEY_FN_E),
+ "KEY_FN_F" => Ok(EV_KEY::KEY_FN_F),
+ "KEY_FN_S" => Ok(EV_KEY::KEY_FN_S),
+ "KEY_FN_B" => Ok(EV_KEY::KEY_FN_B),
+ "KEY_FN_RIGHT_SHIFT" => Ok(EV_KEY::KEY_FN_RIGHT_SHIFT),
+ "KEY_BRL_DOT1" => Ok(EV_KEY::KEY_BRL_DOT1),
+ "KEY_BRL_DOT2" => Ok(EV_KEY::KEY_BRL_DOT2),
+ "KEY_BRL_DOT3" => Ok(EV_KEY::KEY_BRL_DOT3),
+ "KEY_BRL_DOT4" => Ok(EV_KEY::KEY_BRL_DOT4),
+ "KEY_BRL_DOT5" => Ok(EV_KEY::KEY_BRL_DOT5),
+ "KEY_BRL_DOT6" => Ok(EV_KEY::KEY_BRL_DOT6),
+ "KEY_BRL_DOT7" => Ok(EV_KEY::KEY_BRL_DOT7),
+ "KEY_BRL_DOT8" => Ok(EV_KEY::KEY_BRL_DOT8),
+ "KEY_BRL_DOT9" => Ok(EV_KEY::KEY_BRL_DOT9),
+ "KEY_BRL_DOT10" => Ok(EV_KEY::KEY_BRL_DOT10),
+ "KEY_NUMERIC_0" => Ok(EV_KEY::KEY_NUMERIC_0),
+ "KEY_NUMERIC_1" => Ok(EV_KEY::KEY_NUMERIC_1),
+ "KEY_NUMERIC_2" => Ok(EV_KEY::KEY_NUMERIC_2),
+ "KEY_NUMERIC_3" => Ok(EV_KEY::KEY_NUMERIC_3),
+ "KEY_NUMERIC_4" => Ok(EV_KEY::KEY_NUMERIC_4),
+ "KEY_NUMERIC_5" => Ok(EV_KEY::KEY_NUMERIC_5),
+ "KEY_NUMERIC_6" => Ok(EV_KEY::KEY_NUMERIC_6),
+ "KEY_NUMERIC_7" => Ok(EV_KEY::KEY_NUMERIC_7),
+ "KEY_NUMERIC_8" => Ok(EV_KEY::KEY_NUMERIC_8),
+ "KEY_NUMERIC_9" => Ok(EV_KEY::KEY_NUMERIC_9),
+ "KEY_NUMERIC_STAR" => Ok(EV_KEY::KEY_NUMERIC_STAR),
+ "KEY_NUMERIC_POUND" => Ok(EV_KEY::KEY_NUMERIC_POUND),
+ "KEY_NUMERIC_A" => Ok(EV_KEY::KEY_NUMERIC_A),
+ "KEY_NUMERIC_B" => Ok(EV_KEY::KEY_NUMERIC_B),
+ "KEY_NUMERIC_C" => Ok(EV_KEY::KEY_NUMERIC_C),
+ "KEY_NUMERIC_D" => Ok(EV_KEY::KEY_NUMERIC_D),
+ "KEY_CAMERA_FOCUS" => Ok(EV_KEY::KEY_CAMERA_FOCUS),
+ "KEY_WPS_BUTTON" => Ok(EV_KEY::KEY_WPS_BUTTON),
+ "KEY_TOUCHPAD_TOGGLE" => Ok(EV_KEY::KEY_TOUCHPAD_TOGGLE),
+ "KEY_TOUCHPAD_ON" => Ok(EV_KEY::KEY_TOUCHPAD_ON),
+ "KEY_TOUCHPAD_OFF" => Ok(EV_KEY::KEY_TOUCHPAD_OFF),
+ "KEY_CAMERA_ZOOMIN" => Ok(EV_KEY::KEY_CAMERA_ZOOMIN),
+ "KEY_CAMERA_ZOOMOUT" => Ok(EV_KEY::KEY_CAMERA_ZOOMOUT),
+ "KEY_CAMERA_UP" => Ok(EV_KEY::KEY_CAMERA_UP),
+ "KEY_CAMERA_DOWN" => Ok(EV_KEY::KEY_CAMERA_DOWN),
+ "KEY_CAMERA_LEFT" => Ok(EV_KEY::KEY_CAMERA_LEFT),
+ "KEY_CAMERA_RIGHT" => Ok(EV_KEY::KEY_CAMERA_RIGHT),
+ "KEY_ATTENDANT_ON" => Ok(EV_KEY::KEY_ATTENDANT_ON),
+ "KEY_ATTENDANT_OFF" => Ok(EV_KEY::KEY_ATTENDANT_OFF),
+ "KEY_ATTENDANT_TOGGLE" => Ok(EV_KEY::KEY_ATTENDANT_TOGGLE),
+ "KEY_LIGHTS_TOGGLE" => Ok(EV_KEY::KEY_LIGHTS_TOGGLE),
+ "KEY_ALS_TOGGLE" => Ok(EV_KEY::KEY_ALS_TOGGLE),
+ "KEY_ROTATE_LOCK_TOGGLE" => Ok(EV_KEY::KEY_ROTATE_LOCK_TOGGLE),
+ "KEY_REFRESH_RATE_TOGGLE" => Ok(EV_KEY::KEY_REFRESH_RATE_TOGGLE),
+ "KEY_BUTTONCONFIG" => Ok(EV_KEY::KEY_BUTTONCONFIG),
+ "KEY_TASKMANAGER" => Ok(EV_KEY::KEY_TASKMANAGER),
+ "KEY_JOURNAL" => Ok(EV_KEY::KEY_JOURNAL),
+ "KEY_CONTROLPANEL" => Ok(EV_KEY::KEY_CONTROLPANEL),
+ "KEY_APPSELECT" => Ok(EV_KEY::KEY_APPSELECT),
+ "KEY_SCREENSAVER" => Ok(EV_KEY::KEY_SCREENSAVER),
+ "KEY_VOICECOMMAND" => Ok(EV_KEY::KEY_VOICECOMMAND),
+ "KEY_ASSISTANT" => Ok(EV_KEY::KEY_ASSISTANT),
+ "KEY_KBD_LAYOUT_NEXT" => Ok(EV_KEY::KEY_KBD_LAYOUT_NEXT),
+ "KEY_EMOJI_PICKER" => Ok(EV_KEY::KEY_EMOJI_PICKER),
+ "KEY_DICTATE" => Ok(EV_KEY::KEY_DICTATE),
+ "KEY_CAMERA_ACCESS_ENABLE" => Ok(EV_KEY::KEY_CAMERA_ACCESS_ENABLE),
+ "KEY_CAMERA_ACCESS_DISABLE" => Ok(EV_KEY::KEY_CAMERA_ACCESS_DISABLE),
+ "KEY_CAMERA_ACCESS_TOGGLE" => Ok(EV_KEY::KEY_CAMERA_ACCESS_TOGGLE),
+ "KEY_ACCESSIBILITY" => Ok(EV_KEY::KEY_ACCESSIBILITY),
+ "KEY_DO_NOT_DISTURB" => Ok(EV_KEY::KEY_DO_NOT_DISTURB),
+ "KEY_BRIGHTNESS_MIN" => Ok(EV_KEY::KEY_BRIGHTNESS_MIN),
+ "KEY_BRIGHTNESS_MAX" => Ok(EV_KEY::KEY_BRIGHTNESS_MAX),
+ "KEY_KBDINPUTASSIST_PREV" => Ok(EV_KEY::KEY_KBDINPUTASSIST_PREV),
+ "KEY_KBDINPUTASSIST_NEXT" => Ok(EV_KEY::KEY_KBDINPUTASSIST_NEXT),
+ "KEY_KBDINPUTASSIST_PREVGROUP" => Ok(EV_KEY::KEY_KBDINPUTASSIST_PREVGROUP),
+ "KEY_KBDINPUTASSIST_NEXTGROUP" => Ok(EV_KEY::KEY_KBDINPUTASSIST_NEXTGROUP),
+ "KEY_KBDINPUTASSIST_ACCEPT" => Ok(EV_KEY::KEY_KBDINPUTASSIST_ACCEPT),
+ "KEY_KBDINPUTASSIST_CANCEL" => Ok(EV_KEY::KEY_KBDINPUTASSIST_CANCEL),
+ "KEY_RIGHT_UP" => Ok(EV_KEY::KEY_RIGHT_UP),
+ "KEY_RIGHT_DOWN" => Ok(EV_KEY::KEY_RIGHT_DOWN),
+ "KEY_LEFT_UP" => Ok(EV_KEY::KEY_LEFT_UP),
+ "KEY_LEFT_DOWN" => Ok(EV_KEY::KEY_LEFT_DOWN),
+ "KEY_ROOT_MENU" => Ok(EV_KEY::KEY_ROOT_MENU),
+ "KEY_MEDIA_TOP_MENU" => Ok(EV_KEY::KEY_MEDIA_TOP_MENU),
+ "KEY_NUMERIC_11" => Ok(EV_KEY::KEY_NUMERIC_11),
+ "KEY_NUMERIC_12" => Ok(EV_KEY::KEY_NUMERIC_12),
+ "KEY_AUDIO_DESC" => Ok(EV_KEY::KEY_AUDIO_DESC),
+ "KEY_3D_MODE" => Ok(EV_KEY::KEY_3D_MODE),
+ "KEY_NEXT_FAVORITE" => Ok(EV_KEY::KEY_NEXT_FAVORITE),
+ "KEY_STOP_RECORD" => Ok(EV_KEY::KEY_STOP_RECORD),
+ "KEY_PAUSE_RECORD" => Ok(EV_KEY::KEY_PAUSE_RECORD),
+ "KEY_VOD" => Ok(EV_KEY::KEY_VOD),
+ "KEY_UNMUTE" => Ok(EV_KEY::KEY_UNMUTE),
+ "KEY_FASTREVERSE" => Ok(EV_KEY::KEY_FASTREVERSE),
+ "KEY_SLOWREVERSE" => Ok(EV_KEY::KEY_SLOWREVERSE),
+ "KEY_DATA" => Ok(EV_KEY::KEY_DATA),
+ "KEY_ONSCREEN_KEYBOARD" => Ok(EV_KEY::KEY_ONSCREEN_KEYBOARD),
+ "KEY_PRIVACY_SCREEN_TOGGLE" => Ok(EV_KEY::KEY_PRIVACY_SCREEN_TOGGLE),
+ "KEY_SELECTIVE_SCREENSHOT" => Ok(EV_KEY::KEY_SELECTIVE_SCREENSHOT),
+ "KEY_NEXT_ELEMENT" => Ok(EV_KEY::KEY_NEXT_ELEMENT),
+ "KEY_PREVIOUS_ELEMENT" => Ok(EV_KEY::KEY_PREVIOUS_ELEMENT),
+ "KEY_AUTOPILOT_ENGAGE_TOGGLE" => Ok(EV_KEY::KEY_AUTOPILOT_ENGAGE_TOGGLE),
+ "KEY_MARK_WAYPOINT" => Ok(EV_KEY::KEY_MARK_WAYPOINT),
+ "KEY_SOS" => Ok(EV_KEY::KEY_SOS),
+ "KEY_NAV_CHART" => Ok(EV_KEY::KEY_NAV_CHART),
+ "KEY_FISHING_CHART" => Ok(EV_KEY::KEY_FISHING_CHART),
+ "KEY_SINGLE_RANGE_RADAR" => Ok(EV_KEY::KEY_SINGLE_RANGE_RADAR),
+ "KEY_DUAL_RANGE_RADAR" => Ok(EV_KEY::KEY_DUAL_RANGE_RADAR),
+ "KEY_RADAR_OVERLAY" => Ok(EV_KEY::KEY_RADAR_OVERLAY),
+ "KEY_TRADITIONAL_SONAR" => Ok(EV_KEY::KEY_TRADITIONAL_SONAR),
+ "KEY_CLEARVU_SONAR" => Ok(EV_KEY::KEY_CLEARVU_SONAR),
+ "KEY_SIDEVU_SONAR" => Ok(EV_KEY::KEY_SIDEVU_SONAR),
+ "KEY_NAV_INFO" => Ok(EV_KEY::KEY_NAV_INFO),
+ "KEY_BRIGHTNESS_MENU" => Ok(EV_KEY::KEY_BRIGHTNESS_MENU),
+ "KEY_MACRO1" => Ok(EV_KEY::KEY_MACRO1),
+ "KEY_MACRO2" => Ok(EV_KEY::KEY_MACRO2),
+ "KEY_MACRO3" => Ok(EV_KEY::KEY_MACRO3),
+ "KEY_MACRO4" => Ok(EV_KEY::KEY_MACRO4),
+ "KEY_MACRO5" => Ok(EV_KEY::KEY_MACRO5),
+ "KEY_MACRO6" => Ok(EV_KEY::KEY_MACRO6),
+ "KEY_MACRO7" => Ok(EV_KEY::KEY_MACRO7),
+ "KEY_MACRO8" => Ok(EV_KEY::KEY_MACRO8),
+ "KEY_MACRO9" => Ok(EV_KEY::KEY_MACRO9),
+ "KEY_MACRO10" => Ok(EV_KEY::KEY_MACRO10),
+ "KEY_MACRO11" => Ok(EV_KEY::KEY_MACRO11),
+ "KEY_MACRO12" => Ok(EV_KEY::KEY_MACRO12),
+ "KEY_MACRO13" => Ok(EV_KEY::KEY_MACRO13),
+ "KEY_MACRO14" => Ok(EV_KEY::KEY_MACRO14),
+ "KEY_MACRO15" => Ok(EV_KEY::KEY_MACRO15),
+ "KEY_MACRO16" => Ok(EV_KEY::KEY_MACRO16),
+ "KEY_MACRO17" => Ok(EV_KEY::KEY_MACRO17),
+ "KEY_MACRO18" => Ok(EV_KEY::KEY_MACRO18),
+ "KEY_MACRO19" => Ok(EV_KEY::KEY_MACRO19),
+ "KEY_MACRO20" => Ok(EV_KEY::KEY_MACRO20),
+ "KEY_MACRO21" => Ok(EV_KEY::KEY_MACRO21),
+ "KEY_MACRO22" => Ok(EV_KEY::KEY_MACRO22),
+ "KEY_MACRO23" => Ok(EV_KEY::KEY_MACRO23),
+ "KEY_MACRO24" => Ok(EV_KEY::KEY_MACRO24),
+ "KEY_MACRO25" => Ok(EV_KEY::KEY_MACRO25),
+ "KEY_MACRO26" => Ok(EV_KEY::KEY_MACRO26),
+ "KEY_MACRO27" => Ok(EV_KEY::KEY_MACRO27),
+ "KEY_MACRO28" => Ok(EV_KEY::KEY_MACRO28),
+ "KEY_MACRO29" => Ok(EV_KEY::KEY_MACRO29),
+ "KEY_MACRO30" => Ok(EV_KEY::KEY_MACRO30),
+ "KEY_MACRO_RECORD_START" => Ok(EV_KEY::KEY_MACRO_RECORD_START),
+ "KEY_MACRO_RECORD_STOP" => Ok(EV_KEY::KEY_MACRO_RECORD_STOP),
+ "KEY_MACRO_PRESET_CYCLE" => Ok(EV_KEY::KEY_MACRO_PRESET_CYCLE),
+ "KEY_MACRO_PRESET1" => Ok(EV_KEY::KEY_MACRO_PRESET1),
+ "KEY_MACRO_PRESET2" => Ok(EV_KEY::KEY_MACRO_PRESET2),
+ "KEY_MACRO_PRESET3" => Ok(EV_KEY::KEY_MACRO_PRESET3),
+ "KEY_KBD_LCD_MENU1" => Ok(EV_KEY::KEY_KBD_LCD_MENU1),
+ "KEY_KBD_LCD_MENU2" => Ok(EV_KEY::KEY_KBD_LCD_MENU2),
+ "KEY_KBD_LCD_MENU3" => Ok(EV_KEY::KEY_KBD_LCD_MENU3),
+ "KEY_KBD_LCD_MENU4" => Ok(EV_KEY::KEY_KBD_LCD_MENU4),
+ "KEY_KBD_LCD_MENU5" => Ok(EV_KEY::KEY_KBD_LCD_MENU5),
+ "KEY_MAX" => Ok(EV_KEY::KEY_MAX),
+ "BTN_0" => Ok(EV_KEY::BTN_0),
+ "BTN_1" => Ok(EV_KEY::BTN_1),
+ "BTN_2" => Ok(EV_KEY::BTN_2),
+ "BTN_3" => Ok(EV_KEY::BTN_3),
+ "BTN_4" => Ok(EV_KEY::BTN_4),
+ "BTN_5" => Ok(EV_KEY::BTN_5),
+ "BTN_6" => Ok(EV_KEY::BTN_6),
+ "BTN_7" => Ok(EV_KEY::BTN_7),
+ "BTN_8" => Ok(EV_KEY::BTN_8),
+ "BTN_9" => Ok(EV_KEY::BTN_9),
+ "BTN_LEFT" => Ok(EV_KEY::BTN_LEFT),
+ "BTN_RIGHT" => Ok(EV_KEY::BTN_RIGHT),
+ "BTN_MIDDLE" => Ok(EV_KEY::BTN_MIDDLE),
+ "BTN_SIDE" => Ok(EV_KEY::BTN_SIDE),
+ "BTN_EXTRA" => Ok(EV_KEY::BTN_EXTRA),
+ "BTN_FORWARD" => Ok(EV_KEY::BTN_FORWARD),
+ "BTN_BACK" => Ok(EV_KEY::BTN_BACK),
+ "BTN_TASK" => Ok(EV_KEY::BTN_TASK),
+ "BTN_TRIGGER" => Ok(EV_KEY::BTN_TRIGGER),
+ "BTN_THUMB" => Ok(EV_KEY::BTN_THUMB),
+ "BTN_THUMB2" => Ok(EV_KEY::BTN_THUMB2),
+ "BTN_TOP" => Ok(EV_KEY::BTN_TOP),
+ "BTN_TOP2" => Ok(EV_KEY::BTN_TOP2),
+ "BTN_PINKIE" => Ok(EV_KEY::BTN_PINKIE),
+ "BTN_BASE" => Ok(EV_KEY::BTN_BASE),
+ "BTN_BASE2" => Ok(EV_KEY::BTN_BASE2),
+ "BTN_BASE3" => Ok(EV_KEY::BTN_BASE3),
+ "BTN_BASE4" => Ok(EV_KEY::BTN_BASE4),
+ "BTN_BASE5" => Ok(EV_KEY::BTN_BASE5),
+ "BTN_BASE6" => Ok(EV_KEY::BTN_BASE6),
+ "BTN_DEAD" => Ok(EV_KEY::BTN_DEAD),
+ "BTN_SOUTH" => Ok(EV_KEY::BTN_SOUTH),
+ "BTN_EAST" => Ok(EV_KEY::BTN_EAST),
+ "BTN_C" => Ok(EV_KEY::BTN_C),
+ "BTN_NORTH" => Ok(EV_KEY::BTN_NORTH),
+ "BTN_WEST" => Ok(EV_KEY::BTN_WEST),
+ "BTN_Z" => Ok(EV_KEY::BTN_Z),
+ "BTN_TL" => Ok(EV_KEY::BTN_TL),
+ "BTN_TR" => Ok(EV_KEY::BTN_TR),
+ "BTN_TL2" => Ok(EV_KEY::BTN_TL2),
+ "BTN_TR2" => Ok(EV_KEY::BTN_TR2),
+ "BTN_SELECT" => Ok(EV_KEY::BTN_SELECT),
+ "BTN_START" => Ok(EV_KEY::BTN_START),
+ "BTN_MODE" => Ok(EV_KEY::BTN_MODE),
+ "BTN_THUMBL" => Ok(EV_KEY::BTN_THUMBL),
+ "BTN_THUMBR" => Ok(EV_KEY::BTN_THUMBR),
+ "BTN_TOOL_PEN" => Ok(EV_KEY::BTN_TOOL_PEN),
+ "BTN_TOOL_RUBBER" => Ok(EV_KEY::BTN_TOOL_RUBBER),
+ "BTN_TOOL_BRUSH" => Ok(EV_KEY::BTN_TOOL_BRUSH),
+ "BTN_TOOL_PENCIL" => Ok(EV_KEY::BTN_TOOL_PENCIL),
+ "BTN_TOOL_AIRBRUSH" => Ok(EV_KEY::BTN_TOOL_AIRBRUSH),
+ "BTN_TOOL_FINGER" => Ok(EV_KEY::BTN_TOOL_FINGER),
+ "BTN_TOOL_MOUSE" => Ok(EV_KEY::BTN_TOOL_MOUSE),
+ "BTN_TOOL_LENS" => Ok(EV_KEY::BTN_TOOL_LENS),
+ "BTN_TOOL_QUINTTAP" => Ok(EV_KEY::BTN_TOOL_QUINTTAP),
+ "BTN_STYLUS3" => Ok(EV_KEY::BTN_STYLUS3),
+ "BTN_TOUCH" => Ok(EV_KEY::BTN_TOUCH),
+ "BTN_STYLUS" => Ok(EV_KEY::BTN_STYLUS),
+ "BTN_STYLUS2" => Ok(EV_KEY::BTN_STYLUS2),
+ "BTN_TOOL_DOUBLETAP" => Ok(EV_KEY::BTN_TOOL_DOUBLETAP),
+ "BTN_TOOL_TRIPLETAP" => Ok(EV_KEY::BTN_TOOL_TRIPLETAP),
+ "BTN_TOOL_QUADTAP" => Ok(EV_KEY::BTN_TOOL_QUADTAP),
+ "BTN_GEAR_DOWN" => Ok(EV_KEY::BTN_GEAR_DOWN),
+ "BTN_GEAR_UP" => Ok(EV_KEY::BTN_GEAR_UP),
+ "BTN_DPAD_UP" => Ok(EV_KEY::BTN_DPAD_UP),
+ "BTN_DPAD_DOWN" => Ok(EV_KEY::BTN_DPAD_DOWN),
+ "BTN_DPAD_LEFT" => Ok(EV_KEY::BTN_DPAD_LEFT),
+ "BTN_DPAD_RIGHT" => Ok(EV_KEY::BTN_DPAD_RIGHT),
+ "BTN_TRIGGER_HAPPY1" => Ok(EV_KEY::BTN_TRIGGER_HAPPY1),
+ "BTN_TRIGGER_HAPPY2" => Ok(EV_KEY::BTN_TRIGGER_HAPPY2),
+ "BTN_TRIGGER_HAPPY3" => Ok(EV_KEY::BTN_TRIGGER_HAPPY3),
+ "BTN_TRIGGER_HAPPY4" => Ok(EV_KEY::BTN_TRIGGER_HAPPY4),
+ "BTN_TRIGGER_HAPPY5" => Ok(EV_KEY::BTN_TRIGGER_HAPPY5),
+ "BTN_TRIGGER_HAPPY6" => Ok(EV_KEY::BTN_TRIGGER_HAPPY6),
+ "BTN_TRIGGER_HAPPY7" => Ok(EV_KEY::BTN_TRIGGER_HAPPY7),
+ "BTN_TRIGGER_HAPPY8" => Ok(EV_KEY::BTN_TRIGGER_HAPPY8),
+ "BTN_TRIGGER_HAPPY9" => Ok(EV_KEY::BTN_TRIGGER_HAPPY9),
+ "BTN_TRIGGER_HAPPY10" => Ok(EV_KEY::BTN_TRIGGER_HAPPY10),
+ "BTN_TRIGGER_HAPPY11" => Ok(EV_KEY::BTN_TRIGGER_HAPPY11),
+ "BTN_TRIGGER_HAPPY12" => Ok(EV_KEY::BTN_TRIGGER_HAPPY12),
+ "BTN_TRIGGER_HAPPY13" => Ok(EV_KEY::BTN_TRIGGER_HAPPY13),
+ "BTN_TRIGGER_HAPPY14" => Ok(EV_KEY::BTN_TRIGGER_HAPPY14),
+ "BTN_TRIGGER_HAPPY15" => Ok(EV_KEY::BTN_TRIGGER_HAPPY15),
+ "BTN_TRIGGER_HAPPY16" => Ok(EV_KEY::BTN_TRIGGER_HAPPY16),
+ "BTN_TRIGGER_HAPPY17" => Ok(EV_KEY::BTN_TRIGGER_HAPPY17),
+ "BTN_TRIGGER_HAPPY18" => Ok(EV_KEY::BTN_TRIGGER_HAPPY18),
+ "BTN_TRIGGER_HAPPY19" => Ok(EV_KEY::BTN_TRIGGER_HAPPY19),
+ "BTN_TRIGGER_HAPPY20" => Ok(EV_KEY::BTN_TRIGGER_HAPPY20),
+ "BTN_TRIGGER_HAPPY21" => Ok(EV_KEY::BTN_TRIGGER_HAPPY21),
+ "BTN_TRIGGER_HAPPY22" => Ok(EV_KEY::BTN_TRIGGER_HAPPY22),
+ "BTN_TRIGGER_HAPPY23" => Ok(EV_KEY::BTN_TRIGGER_HAPPY23),
+ "BTN_TRIGGER_HAPPY24" => Ok(EV_KEY::BTN_TRIGGER_HAPPY24),
+ "BTN_TRIGGER_HAPPY25" => Ok(EV_KEY::BTN_TRIGGER_HAPPY25),
+ "BTN_TRIGGER_HAPPY26" => Ok(EV_KEY::BTN_TRIGGER_HAPPY26),
+ "BTN_TRIGGER_HAPPY27" => Ok(EV_KEY::BTN_TRIGGER_HAPPY27),
+ "BTN_TRIGGER_HAPPY28" => Ok(EV_KEY::BTN_TRIGGER_HAPPY28),
+ "BTN_TRIGGER_HAPPY29" => Ok(EV_KEY::BTN_TRIGGER_HAPPY29),
+ "BTN_TRIGGER_HAPPY30" => Ok(EV_KEY::BTN_TRIGGER_HAPPY30),
+ "BTN_TRIGGER_HAPPY31" => Ok(EV_KEY::BTN_TRIGGER_HAPPY31),
+ "BTN_TRIGGER_HAPPY32" => Ok(EV_KEY::BTN_TRIGGER_HAPPY32),
+ "BTN_TRIGGER_HAPPY33" => Ok(EV_KEY::BTN_TRIGGER_HAPPY33),
+ "BTN_TRIGGER_HAPPY34" => Ok(EV_KEY::BTN_TRIGGER_HAPPY34),
+ "BTN_TRIGGER_HAPPY35" => Ok(EV_KEY::BTN_TRIGGER_HAPPY35),
+ "BTN_TRIGGER_HAPPY36" => Ok(EV_KEY::BTN_TRIGGER_HAPPY36),
+ "BTN_TRIGGER_HAPPY37" => Ok(EV_KEY::BTN_TRIGGER_HAPPY37),
+ "BTN_TRIGGER_HAPPY38" => Ok(EV_KEY::BTN_TRIGGER_HAPPY38),
+ "BTN_TRIGGER_HAPPY39" => Ok(EV_KEY::BTN_TRIGGER_HAPPY39),
+ "BTN_TRIGGER_HAPPY40" => Ok(EV_KEY::BTN_TRIGGER_HAPPY40),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_KEY {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_LED {
+ LED_NUML = 0,
+ LED_CAPSL = 1,
+ LED_SCROLLL = 2,
+ LED_COMPOSE = 3,
+ LED_KANA = 4,
+ LED_SLEEP = 5,
+ LED_SUSPEND = 6,
+ LED_MUTE = 7,
+ LED_MISC = 8,
+ LED_MAIL = 9,
+ LED_CHARGING = 10,
+ LED_MAX = 15,
+}
+
+pub const fn int_to_ev_led(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_LED::LED_NUML),
+ 1 => Some(EV_LED::LED_CAPSL),
+ 2 => Some(EV_LED::LED_SCROLLL),
+ 3 => Some(EV_LED::LED_COMPOSE),
+ 4 => Some(EV_LED::LED_KANA),
+ 5 => Some(EV_LED::LED_SLEEP),
+ 6 => Some(EV_LED::LED_SUSPEND),
+ 7 => Some(EV_LED::LED_MUTE),
+ 8 => Some(EV_LED::LED_MISC),
+ 9 => Some(EV_LED::LED_MAIL),
+ 10 => Some(EV_LED::LED_CHARGING),
+ 15 => Some(EV_LED::LED_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_LED {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "LED_NUML" => Ok(EV_LED::LED_NUML),
+ "LED_CAPSL" => Ok(EV_LED::LED_CAPSL),
+ "LED_SCROLLL" => Ok(EV_LED::LED_SCROLLL),
+ "LED_COMPOSE" => Ok(EV_LED::LED_COMPOSE),
+ "LED_KANA" => Ok(EV_LED::LED_KANA),
+ "LED_SLEEP" => Ok(EV_LED::LED_SLEEP),
+ "LED_SUSPEND" => Ok(EV_LED::LED_SUSPEND),
+ "LED_MUTE" => Ok(EV_LED::LED_MUTE),
+ "LED_MISC" => Ok(EV_LED::LED_MISC),
+ "LED_MAIL" => Ok(EV_LED::LED_MAIL),
+ "LED_CHARGING" => Ok(EV_LED::LED_CHARGING),
+ "LED_MAX" => Ok(EV_LED::LED_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_LED {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_SND {
+ SND_CLICK = 0,
+ SND_BELL = 1,
+ SND_TONE = 2,
+ SND_MAX = 7,
+}
+
+pub const fn int_to_ev_snd(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_SND::SND_CLICK),
+ 1 => Some(EV_SND::SND_BELL),
+ 2 => Some(EV_SND::SND_TONE),
+ 7 => Some(EV_SND::SND_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_SND {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "SND_CLICK" => Ok(EV_SND::SND_CLICK),
+ "SND_BELL" => Ok(EV_SND::SND_BELL),
+ "SND_TONE" => Ok(EV_SND::SND_TONE),
+ "SND_MAX" => Ok(EV_SND::SND_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_SND {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_MSC {
+ MSC_SERIAL = 0,
+ MSC_PULSELED = 1,
+ MSC_GESTURE = 2,
+ MSC_RAW = 3,
+ MSC_SCAN = 4,
+ MSC_TIMESTAMP = 5,
+ MSC_MAX = 7,
+}
+
+pub const fn int_to_ev_msc(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_MSC::MSC_SERIAL),
+ 1 => Some(EV_MSC::MSC_PULSELED),
+ 2 => Some(EV_MSC::MSC_GESTURE),
+ 3 => Some(EV_MSC::MSC_RAW),
+ 4 => Some(EV_MSC::MSC_SCAN),
+ 5 => Some(EV_MSC::MSC_TIMESTAMP),
+ 7 => Some(EV_MSC::MSC_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_MSC {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "MSC_SERIAL" => Ok(EV_MSC::MSC_SERIAL),
+ "MSC_PULSELED" => Ok(EV_MSC::MSC_PULSELED),
+ "MSC_GESTURE" => Ok(EV_MSC::MSC_GESTURE),
+ "MSC_RAW" => Ok(EV_MSC::MSC_RAW),
+ "MSC_SCAN" => Ok(EV_MSC::MSC_SCAN),
+ "MSC_TIMESTAMP" => Ok(EV_MSC::MSC_TIMESTAMP),
+ "MSC_MAX" => Ok(EV_MSC::MSC_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_MSC {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_SW {
+ SW_LID = 0,
+ SW_TABLET_MODE = 1,
+ SW_HEADPHONE_INSERT = 2,
+ SW_RFKILL_ALL = 3,
+ SW_MICROPHONE_INSERT = 4,
+ SW_DOCK = 5,
+ SW_LINEOUT_INSERT = 6,
+ SW_JACK_PHYSICAL_INSERT = 7,
+ SW_VIDEOOUT_INSERT = 8,
+ SW_CAMERA_LENS_COVER = 9,
+ SW_KEYPAD_SLIDE = 10,
+ SW_FRONT_PROXIMITY = 11,
+ SW_ROTATE_LOCK = 12,
+ SW_LINEIN_INSERT = 13,
+ SW_MUTE_DEVICE = 14,
+ SW_PEN_INSERTED = 15,
+ SW_MACHINE_COVER = 16,
+}
+
+impl EV_SW {
+ pub const SW_MAX: EV_SW = EV_SW::SW_MACHINE_COVER;
+}
+
+pub const fn int_to_ev_sw(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_SW::SW_LID),
+ 1 => Some(EV_SW::SW_TABLET_MODE),
+ 2 => Some(EV_SW::SW_HEADPHONE_INSERT),
+ 3 => Some(EV_SW::SW_RFKILL_ALL),
+ 4 => Some(EV_SW::SW_MICROPHONE_INSERT),
+ 5 => Some(EV_SW::SW_DOCK),
+ 6 => Some(EV_SW::SW_LINEOUT_INSERT),
+ 7 => Some(EV_SW::SW_JACK_PHYSICAL_INSERT),
+ 8 => Some(EV_SW::SW_VIDEOOUT_INSERT),
+ 9 => Some(EV_SW::SW_CAMERA_LENS_COVER),
+ 10 => Some(EV_SW::SW_KEYPAD_SLIDE),
+ 11 => Some(EV_SW::SW_FRONT_PROXIMITY),
+ 12 => Some(EV_SW::SW_ROTATE_LOCK),
+ 13 => Some(EV_SW::SW_LINEIN_INSERT),
+ 14 => Some(EV_SW::SW_MUTE_DEVICE),
+ 15 => Some(EV_SW::SW_PEN_INSERTED),
+ 16 => Some(EV_SW::SW_MACHINE_COVER),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_SW {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "SW_LID" => Ok(EV_SW::SW_LID),
+ "SW_TABLET_MODE" => Ok(EV_SW::SW_TABLET_MODE),
+ "SW_HEADPHONE_INSERT" => Ok(EV_SW::SW_HEADPHONE_INSERT),
+ "SW_RFKILL_ALL" => Ok(EV_SW::SW_RFKILL_ALL),
+ "SW_MICROPHONE_INSERT" => Ok(EV_SW::SW_MICROPHONE_INSERT),
+ "SW_DOCK" => Ok(EV_SW::SW_DOCK),
+ "SW_LINEOUT_INSERT" => Ok(EV_SW::SW_LINEOUT_INSERT),
+ "SW_JACK_PHYSICAL_INSERT" => Ok(EV_SW::SW_JACK_PHYSICAL_INSERT),
+ "SW_VIDEOOUT_INSERT" => Ok(EV_SW::SW_VIDEOOUT_INSERT),
+ "SW_CAMERA_LENS_COVER" => Ok(EV_SW::SW_CAMERA_LENS_COVER),
+ "SW_KEYPAD_SLIDE" => Ok(EV_SW::SW_KEYPAD_SLIDE),
+ "SW_FRONT_PROXIMITY" => Ok(EV_SW::SW_FRONT_PROXIMITY),
+ "SW_ROTATE_LOCK" => Ok(EV_SW::SW_ROTATE_LOCK),
+ "SW_LINEIN_INSERT" => Ok(EV_SW::SW_LINEIN_INSERT),
+ "SW_MUTE_DEVICE" => Ok(EV_SW::SW_MUTE_DEVICE),
+ "SW_PEN_INSERTED" => Ok(EV_SW::SW_PEN_INSERTED),
+ "SW_MACHINE_COVER" => Ok(EV_SW::SW_MACHINE_COVER),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_SW {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_SYN {
+ SYN_REPORT = 0,
+ SYN_CONFIG = 1,
+ SYN_MT_REPORT = 2,
+ SYN_DROPPED = 3,
+ SYN_MAX = 15,
+}
+
+pub const fn int_to_ev_syn(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_SYN::SYN_REPORT),
+ 1 => Some(EV_SYN::SYN_CONFIG),
+ 2 => Some(EV_SYN::SYN_MT_REPORT),
+ 3 => Some(EV_SYN::SYN_DROPPED),
+ 15 => Some(EV_SYN::SYN_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_SYN {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "SYN_REPORT" => Ok(EV_SYN::SYN_REPORT),
+ "SYN_CONFIG" => Ok(EV_SYN::SYN_CONFIG),
+ "SYN_MT_REPORT" => Ok(EV_SYN::SYN_MT_REPORT),
+ "SYN_DROPPED" => Ok(EV_SYN::SYN_DROPPED),
+ "SYN_MAX" => Ok(EV_SYN::SYN_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_SYN {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_REP {
+ REP_DELAY = 0,
+ REP_PERIOD = 1,
+}
+
+impl EV_REP {
+ pub const REP_MAX: EV_REP = EV_REP::REP_PERIOD;
+}
+
+pub const fn int_to_ev_rep(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_REP::REP_DELAY),
+ 1 => Some(EV_REP::REP_PERIOD),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_REP {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "REP_DELAY" => Ok(EV_REP::REP_DELAY),
+ "REP_PERIOD" => Ok(EV_REP::REP_PERIOD),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_REP {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum InputProp {
+ INPUT_PROP_POINTER = 0,
+ INPUT_PROP_DIRECT = 1,
+ INPUT_PROP_BUTTONPAD = 2,
+ INPUT_PROP_SEMI_MT = 3,
+ INPUT_PROP_TOPBUTTONPAD = 4,
+ INPUT_PROP_POINTING_STICK = 5,
+ INPUT_PROP_ACCELEROMETER = 6,
+ INPUT_PROP_MAX = 31,
+}
+
+pub const fn int_to_input_prop(code: u32) -> Option {
+ match code {
+ 0 => Some(InputProp::INPUT_PROP_POINTER),
+ 1 => Some(InputProp::INPUT_PROP_DIRECT),
+ 2 => Some(InputProp::INPUT_PROP_BUTTONPAD),
+ 3 => Some(InputProp::INPUT_PROP_SEMI_MT),
+ 4 => Some(InputProp::INPUT_PROP_TOPBUTTONPAD),
+ 5 => Some(InputProp::INPUT_PROP_POINTING_STICK),
+ 6 => Some(InputProp::INPUT_PROP_ACCELEROMETER),
+ 31 => Some(InputProp::INPUT_PROP_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for InputProp {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "INPUT_PROP_POINTER" => Ok(InputProp::INPUT_PROP_POINTER),
+ "INPUT_PROP_DIRECT" => Ok(InputProp::INPUT_PROP_DIRECT),
+ "INPUT_PROP_BUTTONPAD" => Ok(InputProp::INPUT_PROP_BUTTONPAD),
+ "INPUT_PROP_SEMI_MT" => Ok(InputProp::INPUT_PROP_SEMI_MT),
+ "INPUT_PROP_TOPBUTTONPAD" => Ok(InputProp::INPUT_PROP_TOPBUTTONPAD),
+ "INPUT_PROP_POINTING_STICK" => Ok(InputProp::INPUT_PROP_POINTING_STICK),
+ "INPUT_PROP_ACCELEROMETER" => Ok(InputProp::INPUT_PROP_ACCELEROMETER),
+ "INPUT_PROP_MAX" => Ok(InputProp::INPUT_PROP_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum EV_FF {
+ FF_STATUS_STOPPED = 0,
+ FF_STATUS_PLAYING = 1,
+ FF_RUMBLE = 80,
+ FF_PERIODIC = 81,
+ FF_CONSTANT = 82,
+ FF_SPRING = 83,
+ FF_FRICTION = 84,
+ FF_DAMPER = 85,
+ FF_INERTIA = 86,
+ FF_RAMP = 87,
+ FF_SQUARE = 88,
+ FF_TRIANGLE = 89,
+ FF_SINE = 90,
+ FF_SAW_UP = 91,
+ FF_SAW_DOWN = 92,
+ FF_CUSTOM = 93,
+ FF_GAIN = 96,
+ FF_AUTOCENTER = 97,
+ FF_MAX = 127,
+}
+
+impl EV_FF {
+ pub const FF_STATUS_MAX: EV_FF = EV_FF::FF_STATUS_PLAYING;
+}
+
+pub const fn int_to_ev_ff(code: u32) -> Option {
+ match code {
+ 0 => Some(EV_FF::FF_STATUS_STOPPED),
+ 1 => Some(EV_FF::FF_STATUS_PLAYING),
+ 80 => Some(EV_FF::FF_RUMBLE),
+ 81 => Some(EV_FF::FF_PERIODIC),
+ 82 => Some(EV_FF::FF_CONSTANT),
+ 83 => Some(EV_FF::FF_SPRING),
+ 84 => Some(EV_FF::FF_FRICTION),
+ 85 => Some(EV_FF::FF_DAMPER),
+ 86 => Some(EV_FF::FF_INERTIA),
+ 87 => Some(EV_FF::FF_RAMP),
+ 88 => Some(EV_FF::FF_SQUARE),
+ 89 => Some(EV_FF::FF_TRIANGLE),
+ 90 => Some(EV_FF::FF_SINE),
+ 91 => Some(EV_FF::FF_SAW_UP),
+ 92 => Some(EV_FF::FF_SAW_DOWN),
+ 93 => Some(EV_FF::FF_CUSTOM),
+ 96 => Some(EV_FF::FF_GAIN),
+ 97 => Some(EV_FF::FF_AUTOCENTER),
+ 127 => Some(EV_FF::FF_MAX),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for EV_FF {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "FF_STATUS_STOPPED" => Ok(EV_FF::FF_STATUS_STOPPED),
+ "FF_STATUS_PLAYING" => Ok(EV_FF::FF_STATUS_PLAYING),
+ "FF_RUMBLE" => Ok(EV_FF::FF_RUMBLE),
+ "FF_PERIODIC" => Ok(EV_FF::FF_PERIODIC),
+ "FF_CONSTANT" => Ok(EV_FF::FF_CONSTANT),
+ "FF_SPRING" => Ok(EV_FF::FF_SPRING),
+ "FF_FRICTION" => Ok(EV_FF::FF_FRICTION),
+ "FF_DAMPER" => Ok(EV_FF::FF_DAMPER),
+ "FF_INERTIA" => Ok(EV_FF::FF_INERTIA),
+ "FF_RAMP" => Ok(EV_FF::FF_RAMP),
+ "FF_SQUARE" => Ok(EV_FF::FF_SQUARE),
+ "FF_TRIANGLE" => Ok(EV_FF::FF_TRIANGLE),
+ "FF_SINE" => Ok(EV_FF::FF_SINE),
+ "FF_SAW_UP" => Ok(EV_FF::FF_SAW_UP),
+ "FF_SAW_DOWN" => Ok(EV_FF::FF_SAW_DOWN),
+ "FF_CUSTOM" => Ok(EV_FF::FF_CUSTOM),
+ "FF_GAIN" => Ok(EV_FF::FF_GAIN),
+ "FF_AUTOCENTER" => Ok(EV_FF::FF_AUTOCENTER),
+ "FF_MAX" => Ok(EV_FF::FF_MAX),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for EV_FF {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum BusType {
+ BUS_PCI = 1,
+ BUS_ISAPNP = 2,
+ BUS_USB = 3,
+ BUS_HIL = 4,
+ BUS_BLUETOOTH = 5,
+ BUS_VIRTUAL = 6,
+ BUS_ISA = 16,
+ BUS_I8042 = 17,
+ BUS_XTKBD = 18,
+ BUS_RS232 = 19,
+ BUS_GAMEPORT = 20,
+ BUS_PARPORT = 21,
+ BUS_AMIGA = 22,
+ BUS_ADB = 23,
+ BUS_I2C = 24,
+ BUS_HOST = 25,
+ BUS_GSC = 26,
+ BUS_ATARI = 27,
+ BUS_SPI = 28,
+ BUS_RMI = 29,
+ BUS_CEC = 30,
+ BUS_INTEL_ISHTP = 31,
+ BUS_AMD_SFH = 32,
+}
+
+pub const fn int_to_bus_type(code: u32) -> Option {
+ match code {
+ 1 => Some(BusType::BUS_PCI),
+ 2 => Some(BusType::BUS_ISAPNP),
+ 3 => Some(BusType::BUS_USB),
+ 4 => Some(BusType::BUS_HIL),
+ 5 => Some(BusType::BUS_BLUETOOTH),
+ 6 => Some(BusType::BUS_VIRTUAL),
+ 16 => Some(BusType::BUS_ISA),
+ 17 => Some(BusType::BUS_I8042),
+ 18 => Some(BusType::BUS_XTKBD),
+ 19 => Some(BusType::BUS_RS232),
+ 20 => Some(BusType::BUS_GAMEPORT),
+ 21 => Some(BusType::BUS_PARPORT),
+ 22 => Some(BusType::BUS_AMIGA),
+ 23 => Some(BusType::BUS_ADB),
+ 24 => Some(BusType::BUS_I2C),
+ 25 => Some(BusType::BUS_HOST),
+ 26 => Some(BusType::BUS_GSC),
+ 27 => Some(BusType::BUS_ATARI),
+ 28 => Some(BusType::BUS_SPI),
+ 29 => Some(BusType::BUS_RMI),
+ 30 => Some(BusType::BUS_CEC),
+ 31 => Some(BusType::BUS_INTEL_ISHTP),
+ 32 => Some(BusType::BUS_AMD_SFH),
+ _ => None,
+ }
+}
+
+impl std::str::FromStr for BusType {
+ type Err = ();
+ fn from_str(s: &str) -> Result {
+ match s {
+ "BUS_PCI" => Ok(BusType::BUS_PCI),
+ "BUS_ISAPNP" => Ok(BusType::BUS_ISAPNP),
+ "BUS_USB" => Ok(BusType::BUS_USB),
+ "BUS_HIL" => Ok(BusType::BUS_HIL),
+ "BUS_BLUETOOTH" => Ok(BusType::BUS_BLUETOOTH),
+ "BUS_VIRTUAL" => Ok(BusType::BUS_VIRTUAL),
+ "BUS_ISA" => Ok(BusType::BUS_ISA),
+ "BUS_I8042" => Ok(BusType::BUS_I8042),
+ "BUS_XTKBD" => Ok(BusType::BUS_XTKBD),
+ "BUS_RS232" => Ok(BusType::BUS_RS232),
+ "BUS_GAMEPORT" => Ok(BusType::BUS_GAMEPORT),
+ "BUS_PARPORT" => Ok(BusType::BUS_PARPORT),
+ "BUS_AMIGA" => Ok(BusType::BUS_AMIGA),
+ "BUS_ADB" => Ok(BusType::BUS_ADB),
+ "BUS_I2C" => Ok(BusType::BUS_I2C),
+ "BUS_HOST" => Ok(BusType::BUS_HOST),
+ "BUS_GSC" => Ok(BusType::BUS_GSC),
+ "BUS_ATARI" => Ok(BusType::BUS_ATARI),
+ "BUS_SPI" => Ok(BusType::BUS_SPI),
+ "BUS_RMI" => Ok(BusType::BUS_RMI),
+ "BUS_CEC" => Ok(BusType::BUS_CEC),
+ "BUS_INTEL_ISHTP" => Ok(BusType::BUS_INTEL_ISHTP),
+ "BUS_AMD_SFH" => Ok(BusType::BUS_AMD_SFH),
+ _ => Err(()),
+ }
+ }
+}
+
+impl std::fmt::Display for BusType {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
diff --git a/evdev/src/main/rust/evdev/src/lib.rs b/evdev/src/main/rust/evdev/src/lib.rs
new file mode 100644
index 0000000000..d2ed7a9196
--- /dev/null
+++ b/evdev/src/main/rust/evdev/src/lib.rs
@@ -0,0 +1,238 @@
+//! This crate is from evdev_rs https://github.com/ndesh26/evdev-rs.
+//! It is copied so I can compile libevdev against the Android NDK and so that
+//! a libevdev submodule isn't required. Also so more complex build steps can be removed.
+//! Rust bindings to libevdev, a wrapper for evdev devices.
+
+#![allow(unused_imports, dead_code, unused_assignments)]
+
+#[macro_use]
+mod macros;
+
+mod device;
+pub mod enums;
+mod libevdev;
+mod uinput;
+pub mod util;
+
+use bitflags::bitflags;
+use libc::{c_uint, suseconds_t, time_t};
+use std::convert::{TryFrom, TryInto};
+use std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH};
+
+use enums::*;
+use util::*;
+
+pub use util::EventCodeIterator;
+pub use util::EventTypeIterator;
+pub use util::InputPropIterator;
+
+#[doc(inline)]
+pub use device::Device;
+#[doc(inline)]
+pub use device::DeviceWrapper;
+#[doc(inline)]
+pub use device::Enable;
+#[doc(inline)]
+pub use device::EnableCodeData;
+#[doc(inline)]
+pub use device::UninitDevice;
+#[doc(inline)]
+pub use uinput::UInputDevice;
+
+pub enum GrabMode {
+ /// Grab the device if not currently grabbed
+ Grab = libevdev::LIBEVDEV_GRAB as isize,
+ /// Ungrab the device if currently grabbed
+ Ungrab = libevdev::LIBEVDEV_UNGRAB as isize,
+}
+
+bitflags! {
+#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+ pub struct ReadFlag: u32 {
+ /// Process data in sync mode
+ const SYNC = 1;
+ /// Process data in normal mode
+ const NORMAL = 2;
+ /// Pretend the next event is a SYN_DROPPED and require the
+ /// caller to sync
+ const FORCE_SYNC = 4;
+ /// The fd is not in O_NONBLOCK and a read may block
+ const BLOCKING = 8;
+ }
+}
+
+#[derive(PartialEq)]
+pub enum ReadStatus {
+ /// `next_event` has finished without an error and an event is available
+ /// for processing.
+ Success = libevdev::LIBEVDEV_READ_STATUS_SUCCESS as isize,
+ /// Depending on the `next_event` read flag:
+ /// libevdev received a SYN_DROPPED from the device, and the caller should
+ /// now resync the device, or, an event has been read in sync mode.
+ Sync = libevdev::LIBEVDEV_READ_STATUS_SYNC as isize,
+}
+
+pub enum LedState {
+ /// Turn the LED on
+ On = libevdev::LIBEVDEV_LED_ON as isize,
+ /// Turn the LED off
+ Off = libevdev::LIBEVDEV_LED_OFF as isize,
+}
+
+#[derive(Debug)]
+pub struct DeviceId {
+ pub bustype: BusType,
+ pub vendor: u16,
+ pub product: u16,
+ pub version: u16,
+}
+
+#[derive(Clone, Copy, Debug)]
+/// used by EVIOCGABS/EVIOCSABS ioctls
+pub struct AbsInfo {
+ /// latest reported value for the axis
+ pub value: i32,
+ /// specifies minimum value for the axis
+ pub minimum: i32,
+ /// specifies maximum value for the axis
+ pub maximum: i32,
+ /// specifies fuzz value that is used to filter noise from
+ /// the event stream
+ pub fuzz: i32,
+ /// values that are within this value will be discarded by
+ /// joydev interface and reported as 0 instead
+ pub flat: i32,
+ /// specifies resolution for the values reported for
+ /// the axis
+ pub resolution: i32,
+}
+
+impl AbsInfo {
+ pub const fn from_raw(absinfo: libevdev::input_absinfo) -> AbsInfo {
+ AbsInfo {
+ value: absinfo.value,
+ minimum: absinfo.minimum,
+ maximum: absinfo.maximum,
+ fuzz: absinfo.fuzz,
+ flat: absinfo.flat,
+ resolution: absinfo.resolution,
+ }
+ }
+
+ pub const fn as_raw(&self) -> libevdev::input_absinfo {
+ libevdev::input_absinfo {
+ value: self.value,
+ minimum: self.minimum,
+ maximum: self.maximum,
+ fuzz: self.fuzz,
+ flat: self.flat,
+ resolution: self.resolution,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Eq, Hash, PartialOrd, Ord, Debug, PartialEq)]
+pub struct TimeVal {
+ pub tv_sec: time_t,
+ pub tv_usec: suseconds_t,
+}
+
+impl TryFrom for TimeVal {
+ type Error = SystemTimeError;
+ fn try_from(system_time: SystemTime) -> Result {
+ let d = system_time.duration_since(UNIX_EPOCH)?;
+ Ok(TimeVal {
+ tv_sec: d.as_secs() as time_t,
+ tv_usec: d.subsec_micros() as suseconds_t,
+ })
+ }
+}
+
+impl TryInto for TimeVal {
+ type Error = ();
+ /// Fails if TimeVal.tv_usec is >= 10^6 or if the TimeVal is outside
+ /// the range of SystemTime
+ fn try_into(self) -> Result {
+ let secs = self.tv_sec.try_into().map_err(|_| ())?;
+ let nanos = (self.tv_usec * 1000).try_into().map_err(|_| ())?;
+ let duration = Duration::new(secs, nanos);
+ UNIX_EPOCH.checked_add(duration).ok_or(())
+ }
+}
+
+impl TimeVal {
+ pub const fn new(tv_sec: time_t, tv_usec: suseconds_t) -> TimeVal {
+ const MICROS_PER_SEC: suseconds_t = 1_000_000;
+ TimeVal {
+ tv_sec: tv_sec + (tv_usec / MICROS_PER_SEC) as time_t,
+ tv_usec: tv_usec % MICROS_PER_SEC,
+ }
+ }
+
+ pub const fn from_raw(timeval: &libc::timeval) -> TimeVal {
+ TimeVal {
+ tv_sec: timeval.tv_sec,
+ tv_usec: timeval.tv_usec,
+ }
+ }
+
+ pub const fn as_raw(&self) -> libc::timeval {
+ libc::timeval {
+ tv_sec: self.tv_sec,
+ tv_usec: self.tv_usec,
+ }
+ }
+}
+
+/// The event structure itself
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct InputEvent {
+ /// The time at which event occurred
+ pub time: TimeVal,
+ pub event_code: EventCode,
+ pub value: i32,
+}
+
+impl InputEvent {
+ pub const fn new(timeval: &TimeVal, code: &EventCode, value: i32) -> InputEvent {
+ InputEvent {
+ time: *timeval,
+ event_code: *code,
+ value,
+ }
+ }
+
+ pub fn event_type(&self) -> Option {
+ int_to_event_type(event_code_to_int(&self.event_code).0)
+ }
+
+ pub fn from_raw(event: &libevdev::input_event) -> InputEvent {
+ let ev_type = event.type_ as u32;
+ let event_code = int_to_event_code(ev_type, event.code as u32);
+ InputEvent {
+ time: TimeVal::from_raw(&event.time),
+ event_code,
+ value: event.value,
+ }
+ }
+
+ pub fn as_raw(&self) -> libevdev::input_event {
+ let (ev_type, ev_code) = event_code_to_int(&self.event_code);
+ libevdev::input_event {
+ time: self.time.as_raw(),
+ type_: ev_type as u16,
+ code: ev_code as u16,
+ value: self.value,
+ }
+ }
+
+ pub fn is_type(&self, ev_type: &EventType) -> bool {
+ unsafe { libevdev::libevdev_event_is_type(&self.as_raw(), *ev_type as c_uint) == 1 }
+ }
+
+ pub fn is_code(&self, code: &EventCode) -> bool {
+ let (ev_type, ev_code) = event_code_to_int(code);
+
+ unsafe { libevdev::libevdev_event_is_code(&self.as_raw(), ev_type, ev_code) == 1 }
+ }
+}
diff --git a/evdev/src/main/rust/evdev/src/libevdev.rs b/evdev/src/main/rust/evdev/src/libevdev.rs
new file mode 100644
index 0000000000..caa6a7ab79
--- /dev/null
+++ b/evdev/src/main/rust/evdev/src/libevdev.rs
@@ -0,0 +1,187 @@
+#![allow(bad_style)]
+#![allow(dead_code)]
+#![allow(improper_ctypes)]
+
+pub use libc::timeval;
+use libc::{c_char, c_int, c_uint, c_ushort, c_void, size_t};
+
+pub type __enum_ty = c_int;
+pub type libevdev_read_flag = __enum_ty;
+pub type libevdev_log_priority = __enum_ty;
+pub type libevdev_grab_mode = __enum_ty;
+pub type libevdev_read_status = __enum_ty;
+pub type libevdev_led_value = __enum_ty;
+pub type libevdev_uinput_open_mode = __enum_ty;
+
+pub const LIBEVDEV_READ_FLAG_SYNC: libevdev_read_flag = 1;
+pub const LIBEVDEV_READ_FLAG_NORMAL: libevdev_read_flag = 2;
+pub const LIBEVDEV_READ_FLAG_FORCE_SYNC: libevdev_read_flag = 4;
+pub const LIBEVDEV_READ_FLAG_BLOCKING: libevdev_read_flag = 8;
+
+pub const LIBEVDEV_LOG_ERROR: libevdev_log_priority = 10;
+pub const LIBEVDEV_LOG_INFO: libevdev_log_priority = 20;
+pub const LIBEVDEV_LOG_DEBUG: libevdev_log_priority = 30;
+
+pub const LIBEVDEV_GRAB: libevdev_grab_mode = 3;
+pub const LIBEVDEV_UNGRAB: libevdev_grab_mode = 4;
+
+pub const LIBEVDEV_READ_STATUS_SUCCESS: libevdev_read_status = 0;
+pub const LIBEVDEV_READ_STATUS_SYNC: libevdev_read_status = 1;
+
+pub const LIBEVDEV_LED_ON: libevdev_led_value = 3;
+pub const LIBEVDEV_LED_OFF: libevdev_led_value = 4;
+
+pub const LIBEVDEV_UINPUT_OPEN_MANAGED: libevdev_uinput_open_mode = -2;
+
+pub enum libevdev {}
+
+pub enum libevdev_uinput {}
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct input_event {
+ pub time: timeval,
+ pub type_: c_ushort,
+ pub code: c_ushort,
+ pub value: c_int,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct input_absinfo {
+ pub value: c_int,
+ pub minimum: c_int,
+ pub maximum: c_int,
+ pub fuzz: c_int,
+ pub flat: c_int,
+ pub resolution: c_int,
+}
+
+extern "C" {
+ pub fn libevdev_new() -> *mut libevdev;
+ pub fn libevdev_new_from_fd(fd: c_int, ctx: *mut *mut libevdev) -> c_int;
+ pub fn libevdev_free(ctx: *mut libevdev);
+ pub fn libevdev_grab(ctx: *mut libevdev, grab: libevdev_grab_mode) -> c_int;
+ pub fn libevdev_set_fd(ctx: *mut libevdev, fd: c_int) -> c_int;
+ pub fn libevdev_change_fd(ctx: *mut libevdev, fd: c_int) -> c_int;
+ pub fn libevdev_get_fd(ctx: *mut libevdev) -> c_int;
+ pub fn libevdev_next_event(ctx: *mut libevdev, flags: c_uint, ev: *mut input_event) -> c_int;
+ pub fn libevdev_has_event_pending(ctx: *mut libevdev) -> c_int;
+ pub fn libevdev_get_name(ctx: *const libevdev) -> *const c_char;
+ pub fn libevdev_set_name(ctx: *mut libevdev, name: *const c_char);
+ pub fn libevdev_get_phys(ctx: *const libevdev) -> *const c_char;
+ pub fn libevdev_set_phys(ctx: *mut libevdev, phys: *const c_char);
+ pub fn libevdev_get_uniq(ctx: *const libevdev) -> *const c_char;
+ pub fn libevdev_set_uniq(ctx: *mut libevdev, uniq: *const c_char);
+ pub fn libevdev_get_id_product(ctx: *const libevdev) -> c_int;
+ pub fn libevdev_set_id_product(ctx: *mut libevdev, product_id: c_int);
+ pub fn libevdev_get_id_vendor(ctx: *const libevdev) -> c_int;
+ pub fn libevdev_set_id_vendor(ctx: *mut libevdev, vendor_id: c_int);
+ pub fn libevdev_get_id_bustype(ctx: *const libevdev) -> c_int;
+ pub fn libevdev_set_id_bustype(ctx: *mut libevdev, bustype: c_int);
+ pub fn libevdev_get_id_version(ctx: *const libevdev) -> c_int;
+ pub fn libevdev_set_id_version(ctx: *mut libevdev, version: c_int);
+ pub fn libevdev_get_driver_version(ctx: *const libevdev) -> c_int;
+ pub fn libevdev_has_property(ctx: *const libevdev, prop: c_uint) -> c_int;
+ pub fn libevdev_enable_property(ctx: *mut libevdev, prop: c_uint) -> c_int;
+ pub fn libevdev_disable_property(ctx: *mut libevdev, prop: c_uint) -> c_int;
+ pub fn libevdev_has_event_type(ctx: *const libevdev, type_: c_uint) -> c_int;
+ pub fn libevdev_has_event_code(ctx: *const libevdev, type_: c_uint, code: c_uint) -> c_int;
+ pub fn libevdev_get_abs_minimum(ctx: *const libevdev, code: c_uint) -> c_int;
+ pub fn libevdev_get_abs_maximum(ctx: *const libevdev, code: c_uint) -> c_int;
+ pub fn libevdev_get_abs_fuzz(ctx: *const libevdev, code: c_uint) -> c_int;
+ pub fn libevdev_get_abs_flat(ctx: *const libevdev, code: c_uint) -> c_int;
+ pub fn libevdev_get_abs_resolution(ctx: *const libevdev, code: c_uint) -> c_int;
+ pub fn libevdev_get_abs_info(ctx: *const libevdev, code: c_uint) -> *const input_absinfo;
+ pub fn libevdev_get_event_value(ctx: *const libevdev, type_: c_uint, code: c_uint) -> c_int;
+ pub fn libevdev_set_event_value(
+ ctx: *mut libevdev,
+ type_: c_uint,
+ code: c_uint,
+ value: c_int,
+ ) -> c_int;
+ pub fn libevdev_fetch_event_value(
+ ctx: *const libevdev,
+ type_: c_uint,
+ code: c_uint,
+ value: *mut c_int,
+ ) -> c_int;
+ pub fn libevdev_get_slot_value(ctx: *const libevdev, slot: c_uint, code: c_uint) -> c_int;
+ pub fn libevdev_set_slot_value(
+ ctx: *mut libevdev,
+ slot: c_uint,
+ code: c_uint,
+ value: c_int,
+ ) -> c_int;
+ pub fn libevdev_fetch_slot_value(
+ ctx: *const libevdev,
+ slot: c_uint,
+ code: c_uint,
+ value: *mut c_int,
+ ) -> c_int;
+ pub fn libevdev_get_num_slots(ctx: *const libevdev) -> c_int;
+ pub fn libevdev_get_current_slot(ctx: *const libevdev) -> c_int;
+ pub fn libevdev_set_abs_minimum(ctx: *mut libevdev, code: c_uint, min: c_int);
+ pub fn libevdev_set_abs_maximum(ctx: *mut libevdev, code: c_uint, max: c_int);
+ pub fn libevdev_set_abs_fuzz(ctx: *mut libevdev, code: c_uint, fuzz: c_int);
+ pub fn libevdev_set_abs_flat(ctx: *mut libevdev, code: c_uint, flat: c_int);
+ pub fn libevdev_set_abs_resolution(ctx: *mut libevdev, code: c_uint, resolution: c_int);
+ pub fn libevdev_set_abs_info(ctx: *mut libevdev, code: c_uint, abs: *const input_absinfo);
+ pub fn libevdev_enable_event_type(ctx: *mut libevdev, type_: c_uint) -> c_int;
+ pub fn libevdev_disable_event_type(ctx: *mut libevdev, type_: c_uint) -> c_int;
+ pub fn libevdev_enable_event_code(
+ ctx: *mut libevdev,
+ type_: c_uint,
+ code: c_uint,
+ data: *const c_void,
+ ) -> c_int;
+ pub fn libevdev_disable_event_code(ctx: *mut libevdev, type_: c_uint, code: c_uint) -> c_int;
+ pub fn libevdev_kernel_set_abs_info(
+ ctx: *mut libevdev,
+ code: c_uint,
+ abs: *const input_absinfo,
+ ) -> c_int;
+ pub fn libevdev_kernel_set_led_value(
+ ctx: *mut libevdev,
+ code: c_uint,
+ value: libevdev_led_value,
+ ) -> c_int;
+ pub fn libevdev_kernel_set_led_values(ctx: *mut libevdev, ...) -> c_int;
+ pub fn libevdev_set_clock_id(ctx: *mut libevdev, clockid: c_int) -> c_int;
+ pub fn libevdev_event_is_type(ev: *const input_event, type_: c_uint) -> c_int;
+ pub fn libevdev_event_is_code(ev: *const input_event, type_: c_uint, code: c_uint) -> c_int;
+ pub fn libevdev_event_type_get_name(type_: c_uint) -> *const c_char;
+ pub fn libevdev_event_code_get_name(type_: c_uint, code: c_uint) -> *const c_char;
+ pub fn libevdev_property_get_name(prop: c_uint) -> *const c_char;
+ pub fn libevdev_event_type_get_max(type_: c_uint) -> c_int;
+ pub fn libevdev_event_type_from_name(name: *const c_char) -> c_int;
+ pub fn libevdev_event_type_from_name_n(name: *const c_char, len: size_t) -> c_int;
+ pub fn libevdev_event_code_from_name(type_: c_uint, name: *const c_char) -> c_int;
+ pub fn libevdev_event_code_from_name_n(
+ type_: c_uint,
+ name: *const c_char,
+ len: size_t,
+ ) -> c_int;
+ pub fn libevdev_property_from_name(name: *const c_char) -> c_int;
+ pub fn libevdev_property_from_name_n(name: *const c_char, len: size_t) -> c_int;
+ pub fn libevdev_get_repeat(
+ ctx: *const libevdev,
+ delay: *mut c_int,
+ period: *mut c_int,
+ ) -> c_int;
+ pub fn libevdev_uinput_create_from_device(
+ ctx: *const libevdev,
+ uinput_fd: c_int,
+ uinput_dev: *mut *mut libevdev_uinput,
+ ) -> c_int;
+ pub fn libevdev_uinput_destroy(uinput_dev: *mut libevdev_uinput);
+ pub fn libevdev_uinput_get_devnode(uinput_dev: *mut libevdev_uinput) -> *const c_char;
+ pub fn libevdev_uinput_get_fd(uinput_dev: *const libevdev_uinput) -> c_int;
+ pub fn libevdev_uinput_get_syspath(uinput_dev: *mut libevdev_uinput) -> *const c_char;
+ pub fn libevdev_uinput_write_event(
+ uinput_dev: *const libevdev_uinput,
+ type_: c_uint,
+ code: c_uint,
+ value: c_int,
+ ) -> c_int;
+}
diff --git a/evdev/src/main/rust/evdev/src/macros.rs b/evdev/src/main/rust/evdev/src/macros.rs
new file mode 100644
index 0000000000..27bc575fed
--- /dev/null
+++ b/evdev/src/main/rust/evdev/src/macros.rs
@@ -0,0 +1,81 @@
+macro_rules! string_getter {
+ ( $( #[$doc:meta], $func_name:ident, $c_func: ident ),* ) => {
+ $(
+ #[$doc]
+ fn $func_name (&self) -> Option<&str> {
+ unsafe {
+ ptr_to_str(libevdev::$c_func(self.raw()))
+ }
+ }
+ )*
+ };
+}
+
+macro_rules! string_setter {
+ ( $( $func_name:ident, $c_func: ident ),* ) => {
+ $(
+ fn $func_name (&self, field: &str) {
+ let field = CString::new(field).unwrap();
+ unsafe {
+ libevdev::$c_func(self.raw(), field.as_ptr())
+ }
+ }
+ )*
+ };
+}
+
+macro_rules! product_getter {
+ ( $( $func_name:ident, $c_func: ident ),* ) => {
+ $(
+ fn $func_name (&self) -> u16 {
+ unsafe {
+ libevdev::$c_func(self.raw()) as u16
+ }
+ }
+ )*
+ };
+}
+
+macro_rules! product_setter {
+ ( $( $func_name:ident, $c_func: ident ),* ) => {
+ $(
+ fn $func_name (&self, field: u16) {
+ unsafe {
+ libevdev::$c_func(self.raw(), field as c_int);
+ }
+ }
+ )*
+ };
+}
+
+macro_rules! abs_getter {
+ ( $( $func_name:ident, $c_func: ident ),* ) => {
+ $(
+ fn $func_name (&self,
+ code: u32) -> std::io::Result {
+ let result = unsafe {
+ libevdev::$c_func(self.raw(), code as c_uint) as i32
+ };
+
+ match result {
+ 0 => Err(std::io::Error::from_raw_os_error(0)),
+ k => Ok(k)
+ }
+ }
+ )*
+ };
+}
+
+macro_rules! abs_setter {
+ ( $( $func_name:ident, $c_func: ident ),* ) => {
+ $(
+ fn $func_name (&self,
+ code: u32,
+ val: i32) {
+ unsafe {
+ libevdev::$c_func(self.raw(), code as c_uint, val as c_int);
+ }
+ }
+ )*
+ };
+}
\ No newline at end of file
diff --git a/evdev/src/main/rust/evdev/src/uinput.rs b/evdev/src/main/rust/evdev/src/uinput.rs
new file mode 100644
index 0000000000..f871b16692
--- /dev/null
+++ b/evdev/src/main/rust/evdev/src/uinput.rs
@@ -0,0 +1,116 @@
+use crate::enums::{EventCode, EventType, EV_SYN};
+use crate::util::*;
+use crate::{device::DeviceWrapper, InputEvent};
+use libc::{c_int, c_uint};
+use std::io;
+use std::os::unix::io::RawFd;
+
+use crate::libevdev;
+
+/// Opaque struct representing an evdev uinput device
+pub struct UInputDevice {
+ raw: *mut libevdev::libevdev_uinput,
+}
+
+unsafe impl Sync for UInputDevice {}
+unsafe impl Send for UInputDevice {}
+
+impl UInputDevice {
+ fn raw(&self) -> *mut libevdev::libevdev_uinput {
+ self.raw
+ }
+
+ /// Create a uinput device based on the given libevdev device.
+ ///
+ /// The uinput device will be an exact copy of the libevdev device, minus
+ /// the bits that uinput doesn't allow to be set.
+ pub fn create_from_device(device: &T) -> io::Result {
+ let mut libevdev_uinput = std::ptr::null_mut();
+ let result = unsafe {
+ libevdev::libevdev_uinput_create_from_device(
+ device.raw(),
+ libevdev::LIBEVDEV_UINPUT_OPEN_MANAGED,
+ &mut libevdev_uinput,
+ )
+ };
+
+ match result {
+ 0 => Ok(UInputDevice {
+ raw: libevdev_uinput,
+ }),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+
+ ///Return the device node representing this uinput device.
+ ///
+ /// This relies on `libevdev_uinput_get_syspath()` to provide a valid syspath.
+ pub fn devnode(&self) -> Option<&str> {
+ unsafe { ptr_to_str(libevdev::libevdev_uinput_get_devnode(self.raw())) }
+ }
+
+ ///Return the syspath representing this uinput device.
+ ///
+ /// If the UI_GET_SYSNAME ioctl not available, libevdev makes an educated
+ /// guess. The UI_GET_SYSNAME ioctl is available since Linux 3.15.
+ ///
+ /// The syspath returned is the one of the input node itself
+ /// (e.g. /sys/devices/virtual/input/input123), not the syspath of the
+ /// device node returned with libevdev_uinput_get_devnode().
+ pub fn syspath(&self) -> Option<&str> {
+ unsafe { ptr_to_str(libevdev::libevdev_uinput_get_syspath(self.raw())) }
+ }
+
+ /// Return the file descriptor used to create this uinput device.
+ ///
+ /// This is the fd pointing to /dev/uinput. This file descriptor may be used
+ /// to write events that are emitted by the uinput device. Closing this file
+ /// descriptor will destroy the uinput device.
+ pub fn as_fd(&self) -> Option {
+ match unsafe { libevdev::libevdev_uinput_get_fd(self.raw()) } {
+ 0 => None,
+ result => Some(result),
+ }
+ }
+
+ #[deprecated(
+ since = "0.5.0",
+ note = "Prefer `as_fd`. Some function names were changed so they
+ more closely match their type signature. See issue 42 for discussion
+ https://github.com/ndesh26/evdev-rs/issues/42"
+ )]
+ pub fn fd(&self) -> Option {
+ self.as_fd()
+ }
+
+ /// Post an event through the uinput device.
+ ///
+ /// It is the caller's responsibility that any event sequence is terminated
+ /// with an EV_SYN/SYN_REPORT/0 event. Otherwise, listeners on the device
+ /// node will not see the events until the next EV_SYN event is posted.
+ pub fn write_event(&self, event_type: u32, code: u32, value: i32) -> io::Result<()> {
+ let result =
+ unsafe { libevdev::libevdev_uinput_write_event(self.raw(), event_type, code, value) };
+
+ match result {
+ 0 => Ok(()),
+ error => Err(io::Error::from_raw_os_error(-error)),
+ }
+ }
+}
+
+impl Drop for UInputDevice {
+ fn drop(&mut self) {
+ unsafe {
+ libevdev::libevdev_uinput_destroy(self.raw());
+ }
+ }
+}
+
+impl std::fmt::Debug for UInputDevice {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.debug_struct("UInputDevice")
+ .field("devnode", &self.devnode())
+ .finish()
+ }
+}
diff --git a/evdev/src/main/rust/evdev/src/util.rs b/evdev/src/main/rust/evdev/src/util.rs
new file mode 100644
index 0000000000..f74fb7c84c
--- /dev/null
+++ b/evdev/src/main/rust/evdev/src/util.rs
@@ -0,0 +1,435 @@
+use crate::enums::*;
+use crate::libevdev as raw;
+use libc::{c_char, c_uint};
+use log;
+use log::warn;
+use std::ffi::{CStr, CString};
+use std::fmt;
+
+pub(crate) unsafe fn ptr_to_str(ptr: *const c_char) -> Option<&'static str> {
+ let slice = CStr::from_ptr(ptr.as_ref()?);
+ let buf = slice.to_bytes();
+ std::str::from_utf8(buf).ok()
+}
+
+pub struct EventTypeIterator {
+ current: EventType,
+}
+
+pub struct EventCodeIterator {
+ current: EventCode,
+}
+
+pub struct InputPropIterator {
+ current: InputProp,
+}
+
+impl EventTypeIterator {
+ pub fn new() -> Self {
+ EventTypeIterator {
+ current: EventType::EV_SYN,
+ }
+ }
+}
+
+impl EventCodeIterator {
+ pub fn new(event_type: &EventType) -> Self {
+ let event_code = match *event_type {
+ EventType::EV_SYN => EventCode::EV_SYN(EV_SYN::SYN_REPORT),
+ EventType::EV_KEY => EventCode::EV_KEY(EV_KEY::KEY_RESERVED),
+ EventType::EV_REL => EventCode::EV_REL(EV_REL::REL_X),
+ EventType::EV_ABS => EventCode::EV_ABS(EV_ABS::ABS_X),
+ EventType::EV_MSC => EventCode::EV_MSC(EV_MSC::MSC_SERIAL),
+ EventType::EV_SW => EventCode::EV_SW(EV_SW::SW_LID),
+ EventType::EV_LED => EventCode::EV_LED(EV_LED::LED_NUML),
+ EventType::EV_SND => EventCode::EV_SND(EV_SND::SND_CLICK),
+ EventType::EV_REP => EventCode::EV_REP(EV_REP::REP_DELAY),
+ EventType::EV_FF => EventCode::EV_FF(EV_FF::FF_STATUS_STOPPED),
+ EventType::EV_FF_STATUS => EventCode::EV_FF_STATUS(EV_FF::FF_STATUS_STOPPED),
+ _ => EventCode::EV_MAX,
+ };
+
+ EventCodeIterator {
+ current: event_code,
+ }
+ }
+}
+
+impl InputPropIterator {
+ pub fn new() -> Self {
+ InputPropIterator {
+ current: InputProp::INPUT_PROP_POINTER,
+ }
+ }
+}
+
+pub fn event_code_to_int(event_code: &EventCode) -> (c_uint, c_uint) {
+ match *event_code {
+ EventCode::EV_SYN(code) => (EventType::EV_SYN as c_uint, code as c_uint),
+ EventCode::EV_KEY(code) => (EventType::EV_KEY as c_uint, code as c_uint),
+ EventCode::EV_REL(code) => (EventType::EV_REL as c_uint, code as c_uint),
+ EventCode::EV_ABS(code) => (EventType::EV_ABS as c_uint, code as c_uint),
+ EventCode::EV_MSC(code) => (EventType::EV_MSC as c_uint, code as c_uint),
+ EventCode::EV_SW(code) => (EventType::EV_SW as c_uint, code as c_uint),
+ EventCode::EV_LED(code) => (EventType::EV_LED as c_uint, code as c_uint),
+ EventCode::EV_SND(code) => (EventType::EV_SND as c_uint, code as c_uint),
+ EventCode::EV_REP(code) => (EventType::EV_REP as c_uint, code as c_uint),
+ EventCode::EV_FF(code) => (EventType::EV_FF as c_uint, code as c_uint),
+ EventCode::EV_FF_STATUS(code) => (EventType::EV_FF_STATUS as c_uint, code as c_uint),
+ EventCode::EV_UNK {
+ event_type,
+ event_code,
+ } => (event_type as c_uint, event_code as c_uint),
+ _ => {
+ warn!("Event code not found");
+ (0, 0)
+ }
+ }
+}
+
+pub fn int_to_event_code(event_type: c_uint, event_code: c_uint) -> EventCode {
+ let ev_type: EventType = int_to_event_type(event_type as u32).unwrap();
+ let code = event_code as u32;
+
+ let ev_code = match ev_type {
+ EventType::EV_SYN => int_to_ev_syn(code).map(EventCode::EV_SYN),
+ EventType::EV_KEY => int_to_ev_key(code).map(EventCode::EV_KEY),
+ EventType::EV_ABS => int_to_ev_abs(code).map(EventCode::EV_ABS),
+ EventType::EV_REL => int_to_ev_rel(code).map(EventCode::EV_REL),
+ EventType::EV_MSC => int_to_ev_msc(code).map(EventCode::EV_MSC),
+ EventType::EV_SW => int_to_ev_sw(code).map(EventCode::EV_SW),
+ EventType::EV_LED => int_to_ev_led(code).map(EventCode::EV_LED),
+ EventType::EV_SND => int_to_ev_snd(code).map(EventCode::EV_SND),
+ EventType::EV_REP => int_to_ev_rep(code).map(EventCode::EV_REP),
+ EventType::EV_FF => int_to_ev_ff(code).map(EventCode::EV_FF),
+ EventType::EV_PWR => Some(EventCode::EV_PWR),
+ EventType::EV_FF_STATUS => int_to_ev_ff(code).map(EventCode::EV_FF_STATUS),
+ EventType::EV_UNK => None,
+ EventType::EV_MAX => Some(EventCode::EV_MAX),
+ };
+
+ ev_code.unwrap_or(EventCode::EV_UNK {
+ event_type,
+ event_code,
+ })
+}
+
+impl fmt::Display for EventType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ unsafe { ptr_to_str(raw::libevdev_event_type_get_name(*self as c_uint)) }.unwrap_or("")
+ )
+ }
+}
+
+impl fmt::Display for EventCode {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let (ev_type, ev_code) = event_code_to_int(self);
+ write!(
+ f,
+ "{}",
+ unsafe { ptr_to_str(raw::libevdev_event_code_get_name(ev_type, ev_code)) }
+ .unwrap_or("")
+ )
+ }
+}
+
+impl fmt::Display for InputProp {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ unsafe { ptr_to_str(raw::libevdev_property_get_name(*self as c_uint)) }.unwrap_or("")
+ )
+ }
+}
+
+impl EventType {
+ pub fn iter(&self) -> EventTypeIterator {
+ EventTypeIterator { current: *self }
+ }
+
+ /// The given type constant for the passed name or Errno if not found.
+ pub fn from_str(name: &str) -> Option {
+ let name = CString::new(name).unwrap();
+ let result = unsafe { raw::libevdev_event_type_from_name(name.as_ptr()) };
+
+ match result {
+ -1 => None,
+ k => int_to_event_type(k as u32),
+ }
+ }
+
+ /// The max value defined for the given event type, e.g. ABS_MAX for a type
+ /// of EV_ABS, or Errno for an invalid type.
+ pub fn get_max(ev_type: &EventType) -> Option {
+ let result = unsafe { raw::libevdev_event_type_get_max(*ev_type as c_uint) };
+
+ match result {
+ k if k < 0 => None,
+ k => Some(k as u32),
+ }
+ }
+}
+
+impl EventCode {
+ pub fn iter(&self) -> EventCodeIterator {
+ EventCodeIterator { current: *self }
+ }
+
+ /// Look up an event code by its type and name. Event codes start with a fixed
+ /// prefix followed by their name (eg., "ABS_X"). The prefix must be included in
+ /// the name. It returns the constant assigned to the event code or Errno if not
+ /// found.
+ pub fn from_str(ev_type: &EventType, name: &str) -> Option {
+ let name = CString::new(name).unwrap();
+ let result =
+ unsafe { raw::libevdev_event_code_from_name(*ev_type as c_uint, name.as_ptr()) };
+
+ match result {
+ -1 => None,
+ k => Some(int_to_event_code(*ev_type as u32, k as u32)),
+ }
+ }
+}
+
+impl InputProp {
+ pub fn iter(&self) -> InputPropIterator {
+ InputPropIterator { current: *self }
+ }
+
+ /// Look up an input property by its name. Properties start with the fixed
+ /// prefix "INPUT_PROP_" followed by their name (eg., "INPUT_PROP_POINTER").
+ /// The prefix must be included in the name. It returns the constant assigned
+ /// to the property or Errno if not found.
+ pub fn from_str(name: &str) -> Option {
+ let name = CString::new(name).unwrap();
+ let result = unsafe { raw::libevdev_property_from_name(name.as_ptr()) };
+
+ match result {
+ -1 => None,
+ k => int_to_input_prop(k as u32),
+ }
+ }
+}
+
+// Iterator trait for the enum iterators
+impl Iterator for EventTypeIterator {
+ type Item = EventType;
+
+ fn next(&mut self) -> Option {
+ match self.current {
+ EventType::EV_MAX => None,
+ _ => {
+ let mut raw_code = (self.current as u32) + 1;
+ loop {
+ match int_to_event_type(raw_code) {
+ // TODO: Find a way to iterate over Unknown types
+ Some(EventType::EV_UNK) => raw_code += 1,
+ Some(x) => {
+ let code = self.current;
+ self.current = x;
+ return Some(code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ }
+ }
+}
+
+impl Iterator for EventCodeIterator {
+ type Item = EventCode;
+
+ fn next(&mut self) -> Option {
+ match self.current {
+ EventCode::EV_SYN(code) => match code {
+ EV_SYN::SYN_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_syn(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_SYN(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_KEY(code) => match code {
+ EV_KEY::KEY_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_key(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_KEY(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_REL(code) => match code {
+ EV_REL::REL_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_rel(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_REL(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_ABS(code) => match code {
+ EV_ABS::ABS_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_abs(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_ABS(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_MSC(code) => match code {
+ EV_MSC::MSC_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_msc(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_MSC(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_SW(code) => match code {
+ EV_SW::SW_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_sw(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_SW(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_LED(code) => match code {
+ EV_LED::LED_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_led(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_LED(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_SND(code) => match code {
+ EV_SND::SND_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_snd(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_SND(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_REP(code) => match code {
+ EV_REP::REP_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_rep(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_REP(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ EventCode::EV_FF(code) => match code {
+ EV_FF::FF_MAX => None,
+ _ => {
+ let mut raw_code = (code as u32) + 1;
+ loop {
+ match int_to_ev_ff(raw_code) {
+ Some(x) => {
+ let ev_code = self.current;
+ self.current = EventCode::EV_FF(x);
+ return Some(ev_code);
+ }
+ None => raw_code += 1,
+ }
+ }
+ }
+ },
+ _ => None,
+ }
+ }
+}
+
+impl Iterator for InputPropIterator {
+ type Item = InputProp;
+
+ fn next(&mut self) -> Option {
+ match self.current {
+ InputProp::INPUT_PROP_MAX => None,
+ _ => {
+ let mut raw_enum = (self.current as u32) + 1;
+ loop {
+ match int_to_input_prop(raw_enum) {
+ Some(x) => {
+ let prop = self.current;
+ self.current = x;
+ return Some(prop);
+ }
+ None => raw_enum += 1,
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/.cargo/config.toml b/evdev/src/main/rust/evdev_manager/.cargo/config.toml
new file mode 100644
index 0000000000..f2084c6b7f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/.cargo/config.toml
@@ -0,0 +1,15 @@
+[target.armv7-linux-androideabi]
+rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
+
+[target.aarch64-linux-android]
+rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
+
+[target.i686-linux-android]
+rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
+
+[target.x86_64-linux-android]
+rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
+
+
+
+
diff --git a/evdev/src/main/rust/evdev_manager/Cargo.lock b/evdev/src/main/rust/evdev_manager/Cargo.lock
new file mode 100644
index 0000000000..efe3500145
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/Cargo.lock
@@ -0,0 +1,558 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "assertables"
+version = "9.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59051ec02907378a67b0ba1b8631121f5388c8dbbb3cec8c749d8f93c2c3c211"
+
+[[package]]
+name = "bimap"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "bytes"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+
+[[package]]
+name = "cc"
+version = "1.2.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
+name = "evdev"
+version = "0.1.0"
+dependencies = [
+ "bitflags 2.10.0",
+ "cc",
+ "libc 1.0.0-alpha.1",
+ "log",
+]
+
+[[package]]
+name = "evdev_manager_core"
+version = "0.1.0"
+dependencies = [
+ "assertables",
+ "bimap",
+ "evdev",
+ "glob",
+ "libc 0.2.177",
+ "log",
+ "mio",
+ "notify",
+ "pretty_assertions",
+ "slab",
+ "tokio",
+]
+
+[[package]]
+name = "evdev_manager_jni"
+version = "0.1.0"
+dependencies = [
+ "evdev",
+ "evdev_manager_core",
+ "jni",
+ "libc 0.2.177",
+ "log",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
+[[package]]
+name = "fsevent-sys"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
+dependencies = [
+ "libc 0.2.177",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "inotify"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
+dependencies = [
+ "bitflags 2.10.0",
+ "inotify-sys",
+ "libc 0.2.177",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc 0.2.177",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "kqueue"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
+dependencies = [
+ "kqueue-sys",
+ "libc 0.2.177",
+]
+
+[[package]]
+name = "kqueue-sys"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc 0.2.177",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.177"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+
+[[package]]
+name = "libc"
+version = "1.0.0-alpha.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7222002e5385b4d9327755661e3847c970e8fbf9dea6da8c57f16e8cfbff53a8"
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "main"
+version = "0.1.0"
+dependencies = [
+ "evdev_manager_jni",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "mio"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+dependencies = [
+ "libc 0.2.177",
+ "log",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "notify"
+version = "8.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
+dependencies = [
+ "bitflags 2.10.0",
+ "fsevent-sys",
+ "inotify",
+ "kqueue",
+ "libc 0.2.177",
+ "log",
+ "mio",
+ "notify-types",
+ "walkdir",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "notify-types"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "syn"
+version = "2.0.110"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio"
+version = "1.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+dependencies = [
+ "pin-project-lite",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
diff --git a/evdev/src/main/rust/evdev_manager/Cargo.toml b/evdev/src/main/rust/evdev_manager/Cargo.toml
new file mode 100644
index 0000000000..6b9edfb09f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/Cargo.toml
@@ -0,0 +1,7 @@
+[workspace]
+members = [
+ "main",
+ "core",
+ "jni",
+]
+resolver = "3"
diff --git a/evdev/src/main/rust/evdev_manager/core/Cargo.toml b/evdev/src/main/rust/evdev_manager/core/Cargo.toml
new file mode 100644
index 0000000000..854d5b5bae
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "evdev_manager_core"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "evdev_manager_core"
+crate-type = ["rlib"]
+
+[dependencies]
+log = "0.4.28"
+evdev = { path = "../../evdev" }
+libc = "0.2.177"
+mio = { version = "1.1.0", features = ["os-poll", "os-ext"] }
+slab = "0.4.11"
+tokio = { version = "1.48.0", features = ["rt", "macros", "rt-multi-thread"] }
+bimap = "0.6.3"
+notify = "8.2.0"
+
+[dev-dependencies]
+glob = "0.3"
+pretty_assertions = "1.4.1"
+assertables = "9.8.2"
diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/android_codes.rs b/evdev/src/main/rust/evdev_manager/core/src/android/android_codes.rs
new file mode 100644
index 0000000000..c88417091d
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/android_codes.rs
@@ -0,0 +1,659 @@
+use std::os::raw::c_uint;
+
+pub const AKEYCODE_UNKNOWN: c_uint = 0;
+pub const AKEYCODE_SOFT_LEFT: c_uint = 1;
+pub const AKEYCODE_SOFT_RIGHT: c_uint = 2;
+pub const AKEYCODE_HOME: c_uint = 3;
+pub const AKEYCODE_BACK: c_uint = 4;
+pub const AKEYCODE_CALL: c_uint = 5;
+pub const AKEYCODE_ENDCALL: c_uint = 6;
+pub const AKEYCODE_0: c_uint = 7;
+pub const AKEYCODE_1: c_uint = 8;
+pub const AKEYCODE_2: c_uint = 9;
+pub const AKEYCODE_3: c_uint = 10;
+pub const AKEYCODE_4: c_uint = 11;
+pub const AKEYCODE_5: c_uint = 12;
+pub const AKEYCODE_6: c_uint = 13;
+pub const AKEYCODE_7: c_uint = 14;
+pub const AKEYCODE_8: c_uint = 15;
+pub const AKEYCODE_9: c_uint = 16;
+pub const AKEYCODE_STAR: c_uint = 17;
+pub const AKEYCODE_POUND: c_uint = 18;
+pub const AKEYCODE_DPAD_UP: c_uint = 19;
+pub const AKEYCODE_DPAD_DOWN: c_uint = 20;
+pub const AKEYCODE_DPAD_LEFT: c_uint = 21;
+pub const AKEYCODE_DPAD_RIGHT: c_uint = 22;
+pub const AKEYCODE_DPAD_CENTER: c_uint = 23;
+pub const AKEYCODE_VOLUME_UP: c_uint = 24;
+pub const AKEYCODE_VOLUME_DOWN: c_uint = 25;
+pub const AKEYCODE_POWER: c_uint = 26;
+pub const AKEYCODE_CAMERA: c_uint = 27;
+pub const AKEYCODE_CLEAR: c_uint = 28;
+pub const AKEYCODE_A: c_uint = 29;
+pub const AKEYCODE_B: c_uint = 30;
+pub const AKEYCODE_C: c_uint = 31;
+pub const AKEYCODE_D: c_uint = 32;
+pub const AKEYCODE_E: c_uint = 33;
+pub const AKEYCODE_F: c_uint = 34;
+pub const AKEYCODE_G: c_uint = 35;
+pub const AKEYCODE_H: c_uint = 36;
+pub const AKEYCODE_I: c_uint = 37;
+pub const AKEYCODE_J: c_uint = 38;
+pub const AKEYCODE_K: c_uint = 39;
+pub const AKEYCODE_L: c_uint = 40;
+pub const AKEYCODE_M: c_uint = 41;
+pub const AKEYCODE_N: c_uint = 42;
+pub const AKEYCODE_O: c_uint = 43;
+pub const AKEYCODE_P: c_uint = 44;
+pub const AKEYCODE_Q: c_uint = 45;
+pub const AKEYCODE_R: c_uint = 46;
+pub const AKEYCODE_S: c_uint = 47;
+pub const AKEYCODE_T: c_uint = 48;
+pub const AKEYCODE_U: c_uint = 49;
+pub const AKEYCODE_V: c_uint = 50;
+pub const AKEYCODE_W: c_uint = 51;
+pub const AKEYCODE_X: c_uint = 52;
+pub const AKEYCODE_Y: c_uint = 53;
+pub const AKEYCODE_Z: c_uint = 54;
+pub const AKEYCODE_COMMA: c_uint = 55;
+pub const AKEYCODE_PERIOD: c_uint = 56;
+pub const AKEYCODE_ALT_LEFT: c_uint = 57;
+pub const AKEYCODE_ALT_RIGHT: c_uint = 58;
+pub const AKEYCODE_SHIFT_LEFT: c_uint = 59;
+pub const AKEYCODE_SHIFT_RIGHT: c_uint = 60;
+pub const AKEYCODE_TAB: c_uint = 61;
+pub const AKEYCODE_SPACE: c_uint = 62;
+pub const AKEYCODE_SYM: c_uint = 63;
+pub const AKEYCODE_EXPLORER: c_uint = 64;
+pub const AKEYCODE_ENVELOPE: c_uint = 65;
+pub const AKEYCODE_ENTER: c_uint = 66;
+pub const AKEYCODE_DEL: c_uint = 67;
+pub const AKEYCODE_GRAVE: c_uint = 68;
+pub const AKEYCODE_MINUS: c_uint = 69;
+pub const AKEYCODE_EQUALS: c_uint = 70;
+pub const AKEYCODE_LEFT_BRACKET: c_uint = 71;
+pub const AKEYCODE_RIGHT_BRACKET: c_uint = 72;
+pub const AKEYCODE_BACKSLASH: c_uint = 73;
+pub const AKEYCODE_SEMICOLON: c_uint = 74;
+pub const AKEYCODE_APOSTROPHE: c_uint = 75;
+pub const AKEYCODE_SLASH: c_uint = 76;
+pub const AKEYCODE_AT: c_uint = 77;
+pub const AKEYCODE_NUM: c_uint = 78;
+pub const AKEYCODE_HEADSETHOOK: c_uint = 79;
+pub const AKEYCODE_FOCUS: c_uint = 80;
+pub const AKEYCODE_PLUS: c_uint = 81;
+pub const AKEYCODE_MENU: c_uint = 82;
+pub const AKEYCODE_NOTIFICATION: c_uint = 83;
+pub const AKEYCODE_SEARCH: c_uint = 84;
+pub const AKEYCODE_MEDIA_PLAY_PAUSE: c_uint = 85;
+pub const AKEYCODE_MEDIA_STOP: c_uint = 86;
+pub const AKEYCODE_MEDIA_NEXT: c_uint = 87;
+pub const AKEYCODE_MEDIA_PREVIOUS: c_uint = 88;
+pub const AKEYCODE_MEDIA_REWIND: c_uint = 89;
+pub const AKEYCODE_MEDIA_FAST_FORWARD: c_uint = 90;
+pub const AKEYCODE_MUTE: c_uint = 91;
+pub const AKEYCODE_PAGE_UP: c_uint = 92;
+pub const AKEYCODE_PAGE_DOWN: c_uint = 93;
+pub const AKEYCODE_PICTSYMBOLS: c_uint = 94;
+pub const AKEYCODE_SWITCH_CHARSET: c_uint = 95;
+pub const AKEYCODE_BUTTON_A: c_uint = 96;
+pub const AKEYCODE_BUTTON_B: c_uint = 97;
+pub const AKEYCODE_BUTTON_C: c_uint = 98;
+pub const AKEYCODE_BUTTON_X: c_uint = 99;
+pub const AKEYCODE_BUTTON_Y: c_uint = 100;
+pub const AKEYCODE_BUTTON_Z: c_uint = 101;
+pub const AKEYCODE_BUTTON_L1: c_uint = 102;
+pub const AKEYCODE_BUTTON_R1: c_uint = 103;
+pub const AKEYCODE_BUTTON_L2: c_uint = 104;
+pub const AKEYCODE_BUTTON_R2: c_uint = 105;
+pub const AKEYCODE_BUTTON_THUMBL: c_uint = 106;
+pub const AKEYCODE_BUTTON_THUMBR: c_uint = 107;
+pub const AKEYCODE_BUTTON_START: c_uint = 108;
+pub const AKEYCODE_BUTTON_SELECT: c_uint = 109;
+pub const AKEYCODE_BUTTON_MODE: c_uint = 110;
+pub const AKEYCODE_ESCAPE: c_uint = 111;
+pub const AKEYCODE_FORWARD_DEL: c_uint = 112;
+pub const AKEYCODE_CTRL_LEFT: c_uint = 113;
+pub const AKEYCODE_CTRL_RIGHT: c_uint = 114;
+pub const AKEYCODE_CAPS_LOCK: c_uint = 115;
+pub const AKEYCODE_SCROLL_LOCK: c_uint = 116;
+pub const AKEYCODE_META_LEFT: c_uint = 117;
+pub const AKEYCODE_META_RIGHT: c_uint = 118;
+pub const AKEYCODE_FUNCTION: c_uint = 119;
+pub const AKEYCODE_SYSRQ: c_uint = 120;
+pub const AKEYCODE_BREAK: c_uint = 121;
+pub const AKEYCODE_MOVE_HOME: c_uint = 122;
+pub const AKEYCODE_MOVE_END: c_uint = 123;
+pub const AKEYCODE_INSERT: c_uint = 124;
+pub const AKEYCODE_FORWARD: c_uint = 125;
+pub const AKEYCODE_MEDIA_PLAY: c_uint = 126;
+pub const AKEYCODE_MEDIA_PAUSE: c_uint = 127;
+pub const AKEYCODE_MEDIA_CLOSE: c_uint = 128;
+pub const AKEYCODE_MEDIA_EJECT: c_uint = 129;
+pub const AKEYCODE_MEDIA_RECORD: c_uint = 130;
+pub const AKEYCODE_F1: c_uint = 131;
+pub const AKEYCODE_F2: c_uint = 132;
+pub const AKEYCODE_F3: c_uint = 133;
+pub const AKEYCODE_F4: c_uint = 134;
+pub const AKEYCODE_F5: c_uint = 135;
+pub const AKEYCODE_F6: c_uint = 136;
+pub const AKEYCODE_F7: c_uint = 137;
+pub const AKEYCODE_F8: c_uint = 138;
+pub const AKEYCODE_F9: c_uint = 139;
+pub const AKEYCODE_F10: c_uint = 140;
+pub const AKEYCODE_F11: c_uint = 141;
+pub const AKEYCODE_F12: c_uint = 142;
+pub const AKEYCODE_NUM_LOCK: c_uint = 143;
+pub const AKEYCODE_NUMPAD_0: c_uint = 144;
+pub const AKEYCODE_NUMPAD_1: c_uint = 145;
+pub const AKEYCODE_NUMPAD_2: c_uint = 146;
+pub const AKEYCODE_NUMPAD_3: c_uint = 147;
+pub const AKEYCODE_NUMPAD_4: c_uint = 148;
+pub const AKEYCODE_NUMPAD_5: c_uint = 149;
+pub const AKEYCODE_NUMPAD_6: c_uint = 150;
+pub const AKEYCODE_NUMPAD_7: c_uint = 151;
+pub const AKEYCODE_NUMPAD_8: c_uint = 152;
+pub const AKEYCODE_NUMPAD_9: c_uint = 153;
+pub const AKEYCODE_NUMPAD_DIVIDE: c_uint = 154;
+pub const AKEYCODE_NUMPAD_MULTIPLY: c_uint = 155;
+pub const AKEYCODE_NUMPAD_SUBTRACT: c_uint = 156;
+pub const AKEYCODE_NUMPAD_ADD: c_uint = 157;
+pub const AKEYCODE_NUMPAD_DOT: c_uint = 158;
+pub const AKEYCODE_NUMPAD_COMMA: c_uint = 159;
+pub const AKEYCODE_NUMPAD_ENTER: c_uint = 160;
+pub const AKEYCODE_NUMPAD_EQUALS: c_uint = 161;
+pub const AKEYCODE_NUMPAD_LEFT_PAREN: c_uint = 162;
+pub const AKEYCODE_NUMPAD_RIGHT_PAREN: c_uint = 163;
+pub const AKEYCODE_VOLUME_MUTE: c_uint = 164;
+pub const AKEYCODE_INFO: c_uint = 165;
+pub const AKEYCODE_CHANNEL_UP: c_uint = 166;
+pub const AKEYCODE_CHANNEL_DOWN: c_uint = 167;
+pub const AKEYCODE_ZOOM_IN: c_uint = 168;
+pub const AKEYCODE_ZOOM_OUT: c_uint = 169;
+pub const AKEYCODE_TV: c_uint = 170;
+pub const AKEYCODE_WINDOW: c_uint = 171;
+pub const AKEYCODE_GUIDE: c_uint = 172;
+pub const AKEYCODE_DVR: c_uint = 173;
+pub const AKEYCODE_BOOKMARK: c_uint = 174;
+pub const AKEYCODE_CAPTIONS: c_uint = 175;
+pub const AKEYCODE_SETTINGS: c_uint = 176;
+pub const AKEYCODE_TV_POWER: c_uint = 177;
+pub const AKEYCODE_TV_INPUT: c_uint = 178;
+pub const AKEYCODE_STB_POWER: c_uint = 179;
+pub const AKEYCODE_STB_INPUT: c_uint = 180;
+pub const AKEYCODE_AVR_POWER: c_uint = 181;
+pub const AKEYCODE_AVR_INPUT: c_uint = 182;
+pub const AKEYCODE_PROG_RED: c_uint = 183;
+pub const AKEYCODE_PROG_GREEN: c_uint = 184;
+pub const AKEYCODE_PROG_YELLOW: c_uint = 185;
+pub const AKEYCODE_PROG_BLUE: c_uint = 186;
+pub const AKEYCODE_APP_SWITCH: c_uint = 187;
+pub const AKEYCODE_BUTTON_1: c_uint = 188;
+pub const AKEYCODE_BUTTON_2: c_uint = 189;
+pub const AKEYCODE_BUTTON_3: c_uint = 190;
+pub const AKEYCODE_BUTTON_4: c_uint = 191;
+pub const AKEYCODE_BUTTON_5: c_uint = 192;
+pub const AKEYCODE_BUTTON_6: c_uint = 193;
+pub const AKEYCODE_BUTTON_7: c_uint = 194;
+pub const AKEYCODE_BUTTON_8: c_uint = 195;
+pub const AKEYCODE_BUTTON_9: c_uint = 196;
+pub const AKEYCODE_BUTTON_10: c_uint = 197;
+pub const AKEYCODE_BUTTON_11: c_uint = 198;
+pub const AKEYCODE_BUTTON_12: c_uint = 199;
+pub const AKEYCODE_BUTTON_13: c_uint = 200;
+pub const AKEYCODE_BUTTON_14: c_uint = 201;
+pub const AKEYCODE_BUTTON_15: c_uint = 202;
+pub const AKEYCODE_BUTTON_16: c_uint = 203;
+pub const AKEYCODE_LANGUAGE_SWITCH: c_uint = 204;
+pub const AKEYCODE_MANNER_MODE: c_uint = 205;
+pub const AKEYCODE_3D_MODE: c_uint = 206;
+pub const AKEYCODE_CONTACTS: c_uint = 207;
+pub const AKEYCODE_CALENDAR: c_uint = 208;
+pub const AKEYCODE_MUSIC: c_uint = 209;
+pub const AKEYCODE_CALCULATOR: c_uint = 210;
+pub const AKEYCODE_ZENKAKU_HANKAKU: c_uint = 211;
+pub const AKEYCODE_EISU: c_uint = 212;
+pub const AKEYCODE_MUHENKAN: c_uint = 213;
+pub const AKEYCODE_HENKAN: c_uint = 214;
+pub const AKEYCODE_KATAKANA_HIRAGANA: c_uint = 215;
+pub const AKEYCODE_YEN: c_uint = 216;
+pub const AKEYCODE_RO: c_uint = 217;
+pub const AKEYCODE_KANA: c_uint = 218;
+pub const AKEYCODE_ASSIST: c_uint = 219;
+pub const AKEYCODE_BRIGHTNESS_DOWN: c_uint = 220;
+pub const AKEYCODE_BRIGHTNESS_UP: c_uint = 221;
+pub const AKEYCODE_MEDIA_AUDIO_TRACK: c_uint = 222;
+pub const AKEYCODE_SLEEP: c_uint = 223;
+pub const AKEYCODE_WAKEUP: c_uint = 224;
+pub const AKEYCODE_PAIRING: c_uint = 225;
+pub const AKEYCODE_MEDIA_TOP_MENU: c_uint = 226;
+pub const AKEYCODE_11: c_uint = 227;
+pub const AKEYCODE_12: c_uint = 228;
+pub const AKEYCODE_LAST_CHANNEL: c_uint = 229;
+pub const AKEYCODE_TV_DATA_SERVICE: c_uint = 230;
+pub const AKEYCODE_VOICE_ASSIST: c_uint = 231;
+pub const AKEYCODE_TV_RADIO_SERVICE: c_uint = 232;
+pub const AKEYCODE_TV_TELETEXT: c_uint = 233;
+pub const AKEYCODE_TV_NUMBER_ENTRY: c_uint = 234;
+pub const AKEYCODE_TV_TERRESTRIAL_ANALOG: c_uint = 235;
+pub const AKEYCODE_TV_TERRESTRIAL_DIGITAL: c_uint = 236;
+pub const AKEYCODE_TV_SATELLITE: c_uint = 237;
+pub const AKEYCODE_TV_SATELLITE_BS: c_uint = 238;
+pub const AKEYCODE_TV_SATELLITE_CS: c_uint = 239;
+pub const AKEYCODE_TV_SATELLITE_SERVICE: c_uint = 240;
+pub const AKEYCODE_TV_NETWORK: c_uint = 241;
+pub const AKEYCODE_TV_ANTENNA_CABLE: c_uint = 242;
+pub const AKEYCODE_TV_INPUT_HDMI_1: c_uint = 243;
+pub const AKEYCODE_TV_INPUT_HDMI_2: c_uint = 244;
+pub const AKEYCODE_TV_INPUT_HDMI_3: c_uint = 245;
+pub const AKEYCODE_TV_INPUT_HDMI_4: c_uint = 246;
+pub const AKEYCODE_TV_INPUT_COMPOSITE_1: c_uint = 247;
+pub const AKEYCODE_TV_INPUT_COMPOSITE_2: c_uint = 248;
+pub const AKEYCODE_TV_INPUT_COMPONENT_1: c_uint = 249;
+pub const AKEYCODE_TV_INPUT_COMPONENT_2: c_uint = 250;
+pub const AKEYCODE_TV_INPUT_VGA_1: c_uint = 251;
+pub const AKEYCODE_TV_AUDIO_DESCRIPTION: c_uint = 252;
+pub const AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP: c_uint = 253;
+pub const AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN: c_uint = 254;
+pub const AKEYCODE_TV_ZOOM_MODE: c_uint = 255;
+pub const AKEYCODE_TV_CONTENTS_MENU: c_uint = 256;
+pub const AKEYCODE_TV_MEDIA_CONTEXT_MENU: c_uint = 257;
+pub const AKEYCODE_TV_TIMER_PROGRAMMING: c_uint = 258;
+pub const AKEYCODE_HELP: c_uint = 259;
+pub const AKEYCODE_NAVIGATE_PREVIOUS: c_uint = 260;
+pub const AKEYCODE_NAVIGATE_NEXT: c_uint = 261;
+pub const AKEYCODE_NAVIGATE_IN: c_uint = 262;
+pub const AKEYCODE_NAVIGATE_OUT: c_uint = 263;
+pub const AKEYCODE_STEM_PRIMARY: c_uint = 264;
+pub const AKEYCODE_STEM_1: c_uint = 265;
+pub const AKEYCODE_STEM_2: c_uint = 266;
+pub const AKEYCODE_STEM_3: c_uint = 267;
+pub const AKEYCODE_DPAD_UP_LEFT: c_uint = 268;
+pub const AKEYCODE_DPAD_DOWN_LEFT: c_uint = 269;
+pub const AKEYCODE_DPAD_UP_RIGHT: c_uint = 270;
+pub const AKEYCODE_DPAD_DOWN_RIGHT: c_uint = 271;
+pub const AKEYCODE_MEDIA_SKIP_FORWARD: c_uint = 272;
+pub const AKEYCODE_MEDIA_SKIP_BACKWARD: c_uint = 273;
+pub const AKEYCODE_MEDIA_STEP_FORWARD: c_uint = 274;
+pub const AKEYCODE_MEDIA_STEP_BACKWARD: c_uint = 275;
+pub const AKEYCODE_SOFT_SLEEP: c_uint = 276;
+pub const AKEYCODE_CUT: c_uint = 277;
+pub const AKEYCODE_COPY: c_uint = 278;
+pub const AKEYCODE_PASTE: c_uint = 279;
+pub const AKEYCODE_SYSTEM_NAVIGATION_UP: c_uint = 280;
+pub const AKEYCODE_SYSTEM_NAVIGATION_DOWN: c_uint = 281;
+pub const AKEYCODE_SYSTEM_NAVIGATION_LEFT: c_uint = 282;
+pub const AKEYCODE_SYSTEM_NAVIGATION_RIGHT: c_uint = 283;
+pub const AKEYCODE_ALL_APPS: c_uint = 284;
+pub const AKEYCODE_REFRESH: c_uint = 285;
+pub const AKEYCODE_THUMBS_UP: c_uint = 286;
+pub const AKEYCODE_THUMBS_DOWN: c_uint = 287;
+pub const AKEYCODE_PROFILE_SWITCH: c_uint = 288;
+pub const AKEYCODE_VIDEO_APP_1: c_uint = 289;
+pub const AKEYCODE_VIDEO_APP_2: c_uint = 290;
+pub const AKEYCODE_VIDEO_APP_3: c_uint = 291;
+pub const AKEYCODE_VIDEO_APP_4: c_uint = 292;
+pub const AKEYCODE_VIDEO_APP_5: c_uint = 293;
+pub const AKEYCODE_VIDEO_APP_6: c_uint = 294;
+pub const AKEYCODE_VIDEO_APP_7: c_uint = 295;
+pub const AKEYCODE_VIDEO_APP_8: c_uint = 296;
+pub const AKEYCODE_FEATURED_APP_1: c_uint = 297;
+pub const AKEYCODE_FEATURED_APP_2: c_uint = 298;
+pub const AKEYCODE_FEATURED_APP_3: c_uint = 299;
+pub const AKEYCODE_FEATURED_APP_4: c_uint = 300;
+pub const AKEYCODE_DEMO_APP_1: c_uint = 301;
+pub const AKEYCODE_DEMO_APP_2: c_uint = 302;
+pub const AKEYCODE_DEMO_APP_3: c_uint = 303;
+pub const AKEYCODE_DEMO_APP_4: c_uint = 304;
+pub const AKEYCODE_KEYBOARD_BACKLIGHT_DOWN: c_uint = 305;
+pub const AKEYCODE_KEYBOARD_BACKLIGHT_UP: c_uint = 306;
+pub const AKEYCODE_KEYBOARD_BACKLIGHT_TOGGLE: c_uint = 307;
+pub const AKEYCODE_STYLUS_BUTTON_PRIMARY: c_uint = 308;
+pub const AKEYCODE_STYLUS_BUTTON_SECONDARY: c_uint = 309;
+pub const AKEYCODE_STYLUS_BUTTON_TERTIARY: c_uint = 310;
+pub const AKEYCODE_STYLUS_BUTTON_TAIL: c_uint = 311;
+pub const AKEYCODE_RECENT_APPS: c_uint = 312;
+pub const AKEYCODE_MACRO_1: c_uint = 313;
+pub const AKEYCODE_MACRO_2: c_uint = 314;
+pub const AKEYCODE_MACRO_3: c_uint = 315;
+pub const AKEYCODE_MACRO_4: c_uint = 316;
+
+pub const AMOTION_EVENT_AXIS_X: c_uint = 0;
+/** Axis constant: Y axis of a motion event.
+
+- For a touch screen, reports the absolute Y screen position of the center of
+the touch contact area. The units are display pixels.
+- For a touch pad, reports the absolute Y surface position of the center of the touch
+contact area. The units are device-dependent.
+- For a mouse, reports the absolute Y screen position of the mouse pointer.
+The units are display pixels.
+- For a trackball, reports the relative vertical displacement of the trackball.
+The value is normalized to a range from -1.0 (up) to 1.0 (down).
+- For a joystick, reports the absolute Y position of the joystick.
+The value is normalized to a range from -1.0 (up or far) to 1.0 (down or near).*/
+pub const AMOTION_EVENT_AXIS_Y: c_uint = 1;
+/** Axis constant: Pressure axis of a motion event.
+
+- For a touch screen or touch pad, reports the approximate pressure applied to the surface
+by a finger or other tool. The value is normalized to a range from
+0 (no pressure at all) to 1 (normal pressure), although values higher than 1
+may be generated depending on the calibration of the input device.
+- For a trackball, the value is set to 1 if the trackball button is pressed
+or 0 otherwise.
+- For a mouse, the value is set to 1 if the primary mouse button is pressed
+or 0 otherwise.*/
+pub const AMOTION_EVENT_AXIS_PRESSURE: c_uint = 2;
+/** Axis constant: Size axis of a motion event.
+
+- For a touch screen or touch pad, reports the approximate size of the contact area in
+relation to the maximum detectable size for the device. The value is normalized
+to a range from 0 (smallest detectable size) to 1 (largest detectable size),
+although it is not a linear scale. This value is of limited use.
+To obtain calibrated size information, see
+{@link AMOTION_EVENT_AXIS_TOUCH_MAJOR} or {@link AMOTION_EVENT_AXIS_TOOL_MAJOR}.*/
+pub const AMOTION_EVENT_AXIS_SIZE: c_uint = 3;
+/** Axis constant: TouchMajor axis of a motion event.
+
+- For a touch screen, reports the length of the major axis of an ellipse that
+represents the touch area at the point of contact.
+The units are display pixels.
+- For a touch pad, reports the length of the major axis of an ellipse that
+represents the touch area at the point of contact.
+The units are device-dependent.*/
+pub const AMOTION_EVENT_AXIS_TOUCH_MAJOR: c_uint = 4;
+/** Axis constant: TouchMinor axis of a motion event.
+
+- For a touch screen, reports the length of the minor axis of an ellipse that
+represents the touch area at the point of contact.
+The units are display pixels.
+- For a touch pad, reports the length of the minor axis of an ellipse that
+represents the touch area at the point of contact.
+The units are device-dependent.
+
+When the touch is circular, the major and minor axis lengths will be equal to one another.*/
+pub const AMOTION_EVENT_AXIS_TOUCH_MINOR: c_uint = 5;
+/** Axis constant: ToolMajor axis of a motion event.
+
+- For a touch screen, reports the length of the major axis of an ellipse that
+represents the size of the approaching finger or tool used to make contact.
+- For a touch pad, reports the length of the major axis of an ellipse that
+represents the size of the approaching finger or tool used to make contact.
+The units are device-dependent.
+
+When the touch is circular, the major and minor axis lengths will be equal to one another.
+
+The tool size may be larger than the touch size since the tool may not be fully
+in contact with the touch sensor.*/
+pub const AMOTION_EVENT_AXIS_TOOL_MAJOR: c_uint = 6;
+/** Axis constant: ToolMinor axis of a motion event.
+
+- For a touch screen, reports the length of the minor axis of an ellipse that
+represents the size of the approaching finger or tool used to make contact.
+- For a touch pad, reports the length of the minor axis of an ellipse that
+represents the size of the approaching finger or tool used to make contact.
+The units are device-dependent.
+
+When the touch is circular, the major and minor axis lengths will be equal to one another.
+
+The tool size may be larger than the touch size since the tool may not be fully
+in contact with the touch sensor.*/
+pub const AMOTION_EVENT_AXIS_TOOL_MINOR: c_uint = 7;
+/** Axis constant: Orientation axis of a motion event.
+
+- For a touch screen or touch pad, reports the orientation of the finger
+or tool in radians relative to the vertical plane of the device.
+An angle of 0 radians indicates that the major axis of contact is oriented
+upwards, is perfectly circular or is of unknown orientation. A positive angle
+indicates that the major axis of contact is oriented to the right. A negative angle
+indicates that the major axis of contact is oriented to the left.
+The full range is from -PI/2 radians (finger pointing fully left) to PI/2 radians
+(finger pointing fully right).
+- For a stylus, the orientation indicates the direction in which the stylus
+is pointing in relation to the vertical axis of the current orientation of the screen.
+The range is from -PI radians to PI radians, where 0 is pointing up,
+-PI/2 radians is pointing left, -PI or PI radians is pointing down, and PI/2 radians
+is pointing right. See also #AMOTION_EVENT_AXIS_TILT.*/
+pub const AMOTION_EVENT_AXIS_ORIENTATION: c_uint = 8;
+/** Axis constant: Vertical Scroll axis of a motion event.
+
+- For a mouse, reports the relative movement of the vertical scroll wheel.
+The value is normalized to a range from -1.0 (down) to 1.0 (up).
+
+This axis should be used to scroll views vertically.*/
+pub const AMOTION_EVENT_AXIS_VSCROLL: c_uint = 9;
+/** Axis constant: Horizontal Scroll axis of a motion event.
+
+- For a mouse, reports the relative movement of the horizontal scroll wheel.
+The value is normalized to a range from -1.0 (left) to 1.0 (right).
+
+This axis should be used to scroll views horizontally.*/
+pub const AMOTION_EVENT_AXIS_HSCROLL: c_uint = 10;
+/** Axis constant: Z axis of a motion event.
+
+- For a joystick, reports the absolute Z position of the joystick.
+The value is normalized to a range from -1.0 (high) to 1.0 (low).
+On game pads with two analog joysticks, this axis is often reinterpreted
+to report the absolute X position of the second joystick instead.*/
+pub const AMOTION_EVENT_AXIS_Z: c_uint = 11;
+/** Axis constant: X Rotation axis of a motion event.
+
+- For a joystick, reports the absolute rotation angle about the X axis.
+The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).*/
+pub const AMOTION_EVENT_AXIS_RX: c_uint = 12;
+/** Axis constant: Y Rotation axis of a motion event.
+
+- For a joystick, reports the absolute rotation angle about the Y axis.
+The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).*/
+pub const AMOTION_EVENT_AXIS_RY: c_uint = 13;
+/** Axis constant: Z Rotation axis of a motion event.
+
+- For a joystick, reports the absolute rotation angle about the Z axis.
+The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise).
+On game pads with two analog joysticks, this axis is often reinterpreted
+to report the absolute Y position of the second joystick instead.*/
+pub const AMOTION_EVENT_AXIS_RZ: c_uint = 14;
+/** Axis constant: Hat X axis of a motion event.
+
+- For a joystick, reports the absolute X position of the directional hat control.
+The value is normalized to a range from -1.0 (left) to 1.0 (right).*/
+pub const AMOTION_EVENT_AXIS_HAT_X: c_uint = 15;
+/** Axis constant: Hat Y axis of a motion event.
+
+- For a joystick, reports the absolute Y position of the directional hat control.
+The value is normalized to a range from -1.0 (up) to 1.0 (down).*/
+pub const AMOTION_EVENT_AXIS_HAT_Y: c_uint = 16;
+/** Axis constant: Left Trigger axis of a motion event.
+
+- For a joystick, reports the absolute position of the left trigger control.
+The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed).*/
+pub const AMOTION_EVENT_AXIS_LTRIGGER: c_uint = 17;
+/** Axis constant: Right Trigger axis of a motion event.
+
+- For a joystick, reports the absolute position of the right trigger control.
+The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed).*/
+pub const AMOTION_EVENT_AXIS_RTRIGGER: c_uint = 18;
+/** Axis constant: Throttle axis of a motion event.
+
+- For a joystick, reports the absolute position of the throttle control.
+The value is normalized to a range from 0.0 (fully open) to 1.0 (fully closed).*/
+pub const AMOTION_EVENT_AXIS_THROTTLE: c_uint = 19;
+/** Axis constant: Rudder axis of a motion event.
+
+- For a joystick, reports the absolute position of the rudder control.
+The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right).*/
+pub const AMOTION_EVENT_AXIS_RUDDER: c_uint = 20;
+/** Axis constant: Wheel axis of a motion event.
+
+- For a joystick, reports the absolute position of the steering wheel control.
+The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right).*/
+pub const AMOTION_EVENT_AXIS_WHEEL: c_uint = 21;
+/** Axis constant: Gas axis of a motion event.
+
+- For a joystick, reports the absolute position of the gas (accelerator) control.
+The value is normalized to a range from 0.0 (no acceleration)
+to 1.0 (maximum acceleration).*/
+pub const AMOTION_EVENT_AXIS_GAS: c_uint = 22;
+/** Axis constant: Brake axis of a motion event.
+
+- For a joystick, reports the absolute position of the brake control.
+The value is normalized to a range from 0.0 (no braking) to 1.0 (maximum braking).*/
+pub const AMOTION_EVENT_AXIS_BRAKE: c_uint = 23;
+/** Axis constant: Distance axis of a motion event.
+
+- For a stylus, reports the distance of the stylus from the screen.
+A value of 0.0 indicates direct contact and larger values indicate increasing
+distance from the surface.*/
+pub const AMOTION_EVENT_AXIS_DISTANCE: c_uint = 24;
+/** Axis constant: Tilt axis of a motion event.
+
+- For a stylus, reports the tilt angle of the stylus in radians where
+0 radians indicates that the stylus is being held perpendicular to the
+surface, and PI/2 radians indicates that the stylus is being held flat
+against the surface.*/
+pub const AMOTION_EVENT_AXIS_TILT: c_uint = 25;
+/** Axis constant: Generic scroll axis of a motion event.
+
+- This is used for scroll axis motion events that can't be classified as strictly
+ vertical or horizontal. The movement of a rotating scroller is an example of this.*/
+pub const AMOTION_EVENT_AXIS_SCROLL: c_uint = 26;
+/** Axis constant: The movement of x position of a motion event.
+
+- For a mouse, reports a difference of x position between the previous position.
+This is useful when pointer is captured, in that case the mouse pointer doesn't
+change the location but this axis reports the difference which allows the app
+to see how the mouse is moved.*/
+pub const AMOTION_EVENT_AXIS_RELATIVE_X: c_uint = 27;
+/** Axis constant: The movement of y position of a motion event.
+
+Same as #AMOTION_EVENT_AXIS_RELATIVE_X, but for y position.*/
+pub const AMOTION_EVENT_AXIS_RELATIVE_Y: c_uint = 28;
+/** Axis constant: Generic 1 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_1: c_uint = 32;
+/** Axis constant: Generic 2 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_2: c_uint = 33;
+/** Axis constant: Generic 3 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_3: c_uint = 34;
+/** Axis constant: Generic 4 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_4: c_uint = 35;
+/** Axis constant: Generic 5 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_5: c_uint = 36;
+/** Axis constant: Generic 6 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_6: c_uint = 37;
+/** Axis constant: Generic 7 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_7: c_uint = 38;
+/** Axis constant: Generic 8 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_8: c_uint = 39;
+/** Axis constant: Generic 9 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_9: c_uint = 40;
+/** Axis constant: Generic 10 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_10: c_uint = 41;
+/** Axis constant: Generic 11 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_11: c_uint = 42;
+/** Axis constant: Generic 12 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_12: c_uint = 43;
+/** Axis constant: Generic 13 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_13: c_uint = 44;
+/** Axis constant: Generic 14 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_14: c_uint = 45;
+/** Axis constant: Generic 15 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_15: c_uint = 46;
+/** Axis constant: Generic 16 axis of a motion event.
+The interpretation of a generic axis is device-specific.*/
+pub const AMOTION_EVENT_AXIS_GENERIC_16: c_uint = 47;
+/** Axis constant: X gesture offset axis of a motion event.
+
+- For a touch pad, reports the distance that a swipe gesture has moved in the X axis, as a
+ proportion of the touch pad's size. For example, if a touch pad is 1000 units wide, and a
+ swipe gesture starts at X = 500 then moves to X = 400, this axis would have a value of
+ -0.1.
+
+These values are relative to the state from the last event, not accumulated, so developers
+should make sure to process this axis value for all batched historical events.
+
+This axis is only set on the first pointer in a motion event.*/
+pub const AMOTION_EVENT_AXIS_GESTURE_X_OFFSET: c_uint = 48;
+/** Axis constant: Y gesture offset axis of a motion event.
+
+The same as {@link AMOTION_EVENT_AXIS_GESTURE_X_OFFSET}, but for the Y axis.*/
+pub const AMOTION_EVENT_AXIS_GESTURE_Y_OFFSET: c_uint = 49;
+/** Axis constant: X scroll distance axis of a motion event.
+
+- For a touch pad, reports the distance that should be scrolled in the X axis as a result of
+ the user's two-finger scroll gesture, in display pixels.
+
+These values are relative to the state from the last event, not accumulated, so developers
+should make sure to process this axis value for all batched historical events.
+
+This axis is only set on the first pointer in a motion event.*/
+pub const AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE: c_uint = 50;
+/** Axis constant: Y scroll distance axis of a motion event.
+
+The same as {@link AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE}, but for the Y axis.*/
+pub const AMOTION_EVENT_AXIS_GESTURE_SCROLL_Y_DISTANCE: c_uint = 51;
+/** Axis constant: pinch scale factor of a motion event.
+
+- For a touch pad, reports the change in distance between the fingers when the user is making
+ a pinch gesture, as a proportion of that distance when the gesture was last reported. For
+ example, if the fingers were 50 units apart and are now 52 units apart, the scale factor
+ would be 1.04.
+
+These values are relative to the state from the last event, not accumulated, so developers
+should make sure to process this axis value for all batched historical events.
+
+This axis is only set on the first pointer in a motion event.*/
+pub const AMOTION_EVENT_AXIS_GESTURE_PINCH_SCALE_FACTOR: c_uint = 52;
+/** Axis constant: the number of fingers being used in a multi-finger swipe gesture.
+
+- For a touch pad, reports the number of fingers being used in a multi-finger swipe gesture
+ (with CLASSIFICATION_MULTI_FINGER_SWIPE).
+
+Since CLASSIFICATION_MULTI_FINGER_SWIPE is a hidden API, so is this axis. It is only set on
+the first pointer in a motion event.*/
+pub const AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT: c_uint = 53;
+
+/* These flags originate in RawEvents and are generally set in the key map.
+ * NOTE: If you want a flag to be able to set in a keylayout file, then you must add it to
+ * InputEventLabels.h as well. */
+
+// Indicates that the event should wake the device.
+pub const POLICY_FLAG_WAKE: c_uint = 0x00000001;
+
+// Indicates that the key is virtual, such as a capacitive button, and should
+// generate haptic feedback. Virtual keys may be suppressed for some time
+// after a recent touch to prevent accidental activation of virtual keys adjacent
+// to the touch screen during an edge swipe.
+
+pub const POLICY_FLAG_VIRTUAL: c_uint = 0x00000002;
+
+// Indicates that the key is the special function modifier.
+pub const POLICY_FLAG_FUNCTION: c_uint = 0x00000004;
+
+// Indicates that the key represents a special gesture that has been detected by
+// the touch firmware or driver. Causes touch events from the same device to be canceled.
+// This policy flag prevents key events from changing touch mode state.
+pub const POLICY_FLAG_GESTURE: c_uint = 0x00000008;
+
+// Indicates that key usage mapping represents a fallback mapping.
+// Fallback mappings cannot be used to definitively determine whether a device
+// supports a key code. For example, a HID device can report a key press
+// as a HID usage code if it is not mapped to any linux key code in the kernel.
+// However, we cannot know which HID usage codes that device supports from
+// userspace through the evdev. We can use fallback mappings to convert HID
+// usage codes to Android key codes without needing to know if a device can
+// actually report the usage code.
+pub const POLICY_FLAG_FALLBACK_USAGE_MAPPING: c_uint = 0x00000010;
diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs
new file mode 100644
index 0000000000..ac7f35eb89
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/generic_key_layout.rs
@@ -0,0 +1,288 @@
+//! Hardcoded Generic key layout map based on AOSP Generic.kl.
+//!
+//! This provides a fallback key layout when no device-specific or system Generic.kl
+//! file is available.
+//!
+//! Source: https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/data/keyboards/Generic.kl
+
+/// Generic key layout content matching AOSP Generic.kl
+pub const GENERIC_KEY_LAYOUT_CONTENTS: &str = r#"
+key 1 ESCAPE
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+key 12 MINUS
+key 13 EQUALS
+key 14 DEL
+key 15 TAB
+key 16 Q
+key 17 W
+key 18 E
+key 19 R
+key 20 T
+key 21 Y
+key 22 U
+key 23 I
+key 24 O
+key 25 P
+key 26 LEFT_BRACKET
+key 27 RIGHT_BRACKET
+key 28 ENTER
+key 29 CTRL_LEFT
+key 30 A
+key 31 S
+key 32 D
+key 33 F
+key 34 G
+key 35 H
+key 36 J
+key 37 K
+key 38 L
+key 39 SEMICOLON
+key 40 APOSTROPHE
+key 41 GRAVE
+key 42 SHIFT_LEFT
+key 43 BACKSLASH
+key 44 Z
+key 45 X
+key 46 C
+key 47 V
+key 48 B
+key 49 N
+key 50 M
+key 51 COMMA
+key 52 PERIOD
+key 53 SLASH
+key 54 SHIFT_RIGHT
+key 55 NUMPAD_MULTIPLY
+key 56 ALT_LEFT
+key 57 SPACE
+key 58 CAPS_LOCK
+key 59 F1
+key 60 F2
+key 61 F3
+key 62 F4
+key 63 F5
+key 64 F6
+key 65 F7
+key 66 F8
+key 67 F9
+key 68 F10
+key 69 NUM_LOCK
+key 70 SCROLL_LOCK
+key 71 NUMPAD_7
+key 72 NUMPAD_8
+key 73 NUMPAD_9
+key 74 NUMPAD_SUBTRACT
+key 75 NUMPAD_4
+key 76 NUMPAD_5
+key 77 NUMPAD_6
+key 78 NUMPAD_ADD
+key 79 NUMPAD_1
+key 80 NUMPAD_2
+key 81 NUMPAD_3
+key 82 NUMPAD_0
+key 83 NUMPAD_DOT
+key 85 ZENKAKU_HANKAKU
+key 86 BACKSLASH
+key 87 F11
+key 88 F12
+key 89 RO
+key 92 HENKAN
+key 93 KATAKANA_HIRAGANA
+key 94 MUHENKAN
+key 95 NUMPAD_COMMA
+key 96 NUMPAD_ENTER
+key 97 CTRL_RIGHT
+key 98 NUMPAD_DIVIDE
+key 99 SYSRQ
+key 100 ALT_RIGHT
+key 102 MOVE_HOME
+key 103 DPAD_UP
+key 104 PAGE_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 107 MOVE_END
+key 108 DPAD_DOWN
+key 109 PAGE_DOWN
+key 110 INSERT
+key 111 FORWARD_DEL
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+key 116 POWER
+key 117 NUMPAD_EQUALS
+key 119 BREAK
+key 120 RECENT_APPS
+key 121 NUMPAD_COMMA
+key 122 KANA
+key 123 EISU
+key 124 YEN
+key 125 META_LEFT
+key 126 META_RIGHT
+key 127 MENU
+key 128 MEDIA_STOP
+key 133 COPY
+key 135 PASTE
+key 137 CUT
+key 139 MENU
+key 140 CALCULATOR
+key 142 SLEEP
+key 143 WAKEUP
+key 150 EXPLORER
+key 152 POWER
+key 155 ENVELOPE
+key 156 BOOKMARK
+key 158 BACK
+key 159 FORWARD
+key 160 MEDIA_CLOSE
+key 161 MEDIA_EJECT
+key 162 MEDIA_EJECT
+key 163 MEDIA_NEXT
+key 164 MEDIA_PLAY_PAUSE
+key 165 MEDIA_PREVIOUS
+key 166 MEDIA_STOP
+key 167 MEDIA_RECORD
+key 168 MEDIA_REWIND
+key 169 CALL
+key 171 MUSIC
+key 172 HOME
+key 173 REFRESH
+key 177 PAGE_UP
+key 178 PAGE_DOWN
+key 179 NUMPAD_LEFT_PAREN
+key 180 NUMPAD_RIGHT_PAREN
+key 200 MEDIA_PLAY
+key 201 MEDIA_PAUSE
+key 204 NOTIFICATION
+key 207 MEDIA_PLAY
+key 208 MEDIA_FAST_FORWARD
+key 212 CAMERA
+key 213 MUSIC
+key 215 ENVELOPE
+key 217 SEARCH
+key 224 BRIGHTNESS_DOWN
+key 225 BRIGHTNESS_UP
+key 226 HEADSETHOOK
+key 228 KEYBOARD_BACKLIGHT_TOGGLE
+key 229 KEYBOARD_BACKLIGHT_DOWN
+key 230 KEYBOARD_BACKLIGHT_UP
+key 248 MUTE
+key 256 BUTTON_1
+key 257 BUTTON_2
+key 258 BUTTON_3
+key 259 BUTTON_4
+key 260 BUTTON_5
+key 261 BUTTON_6
+key 262 BUTTON_7
+key 263 BUTTON_8
+key 264 BUTTON_9
+key 265 BUTTON_10
+key 266 BUTTON_11
+key 267 BUTTON_12
+key 268 BUTTON_13
+key 269 BUTTON_14
+key 270 BUTTON_15
+key 271 BUTTON_16
+key 288 BUTTON_1
+key 289 BUTTON_2
+key 290 BUTTON_3
+key 291 BUTTON_4
+key 292 BUTTON_5
+key 293 BUTTON_6
+key 294 BUTTON_7
+key 295 BUTTON_8
+key 296 BUTTON_9
+key 297 BUTTON_10
+key 298 BUTTON_11
+key 299 BUTTON_12
+key 300 BUTTON_13
+key 301 BUTTON_14
+key 302 BUTTON_15
+key 303 BUTTON_16
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 306 BUTTON_C
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 309 BUTTON_Z
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 312 BUTTON_L2
+key 313 BUTTON_R2
+key 314 BUTTON_SELECT
+key 315 BUTTON_START
+key 316 BUTTON_MODE
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+key 329 STYLUS_BUTTON_TERTIARY
+key 331 STYLUS_BUTTON_PRIMARY
+key 332 STYLUS_BUTTON_SECONDARY
+key 353 DPAD_CENTER
+key 362 GUIDE
+key 366 DVR
+key 370 CAPTIONS
+key 377 TV
+key 397 CALENDAR
+key 398 PROG_RED
+key 399 PROG_GREEN
+key 400 PROG_YELLOW
+key 401 PROG_BLUE
+key 402 CHANNEL_UP
+key 403 CHANNEL_DOWN
+key 405 LAST_CHANNEL
+key 418 ZOOM_IN
+key 419 ZOOM_OUT
+key 429 CONTACTS
+key 464 FUNCTION
+key 465 ESCAPE FUNCTION
+key 466 F1 FUNCTION
+key 467 F2 FUNCTION
+key 468 F3 FUNCTION
+key 469 F4 FUNCTION
+key 470 F5 FUNCTION
+key 471 F6 FUNCTION
+key 472 F7 FUNCTION
+key 473 F8 FUNCTION
+key 474 F9 FUNCTION
+key 475 F10 FUNCTION
+key 476 F11 FUNCTION
+key 477 F12 FUNCTION
+key 478 1 FUNCTION
+key 479 2 FUNCTION
+key 480 D FUNCTION
+key 481 E FUNCTION
+key 482 F FUNCTION
+key 483 S FUNCTION
+key 484 B FUNCTION
+key 522 STAR
+key 523 POUND
+key 528 FOCUS
+key 580 APP_SWITCH
+key 582 VOICE_ASSIST
+key 583 ASSIST
+key 656 MACRO_1
+key 657 MACRO_2
+key 658 MACRO_3
+key 659 MACRO_4
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x03 RX
+axis 0x04 RY
+axis 0x05 RZ
+axis 0x06 THROTTLE
+axis 0x07 RUDDER
+axis 0x08 WHEEL
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+"#;
diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/input_event_lookup.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/input_event_lookup.rs
new file mode 100644
index 0000000000..c7db755926
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/input_event_lookup.rs
@@ -0,0 +1,476 @@
+//! Input event label lookup functionality.
+//!
+//! Provides lookup functions for converting between string labels and numeric values
+//! for Android key codes, flags, and axes.
+//!
+//! Uses Android NDK constants (AKEYCODE_*, AMOTION_EVENT_AXIS_*) from the generated bindings.
+//! POLICY_FLAG_* constants are defined locally as they come from the local Input.h header.
+
+use crate::android::android_codes;
+use std::collections::HashMap;
+use std::sync::OnceLock;
+
+// Macro to define keycode entry using AKEYCODE_* constant
+macro_rules! define_keycode {
+ ($name:ident) => {
+ (stringify!($name).to_string(), android_codes::$name as u32)
+ };
+}
+
+// Macro to define axis entry using AMOTION_EVENT_AXIS_* constant
+macro_rules! define_axis {
+ ($name:ident) => {
+ (stringify!($name).to_string(), android_codes::$name as u32)
+ };
+}
+
+// Macro to define policy flag entry using POLICY_FLAG_* constant
+macro_rules! define_flag {
+ ($name:ident) => {
+ (stringify!($name).to_string(), android_codes::$name as u32)
+ };
+}
+
+// Build the keycodes map
+fn build_keycodes_map() -> HashMap {
+ let mut map = HashMap::new();
+
+ // Helper macro to insert keycode
+ macro_rules! insert_keycode {
+ ($name:ident) => {
+ let (name, value) = define_keycode!($name);
+ map.insert(name, value);
+ };
+ }
+
+ insert_keycode!(AKEYCODE_0);
+ insert_keycode!(AKEYCODE_1);
+ insert_keycode!(AKEYCODE_2);
+ insert_keycode!(AKEYCODE_3);
+ insert_keycode!(AKEYCODE_4);
+ insert_keycode!(AKEYCODE_5);
+ insert_keycode!(AKEYCODE_6);
+ insert_keycode!(AKEYCODE_7);
+ insert_keycode!(AKEYCODE_8);
+ insert_keycode!(AKEYCODE_9);
+ insert_keycode!(AKEYCODE_UNKNOWN);
+ insert_keycode!(AKEYCODE_SOFT_LEFT);
+ insert_keycode!(AKEYCODE_SOFT_RIGHT);
+ insert_keycode!(AKEYCODE_HOME);
+ insert_keycode!(AKEYCODE_BACK);
+ insert_keycode!(AKEYCODE_CALL);
+ insert_keycode!(AKEYCODE_ENDCALL);
+ insert_keycode!(AKEYCODE_STAR);
+ insert_keycode!(AKEYCODE_POUND);
+ insert_keycode!(AKEYCODE_DPAD_UP);
+ insert_keycode!(AKEYCODE_DPAD_DOWN);
+ insert_keycode!(AKEYCODE_DPAD_LEFT);
+ insert_keycode!(AKEYCODE_DPAD_RIGHT);
+ insert_keycode!(AKEYCODE_DPAD_CENTER);
+ insert_keycode!(AKEYCODE_VOLUME_UP);
+ insert_keycode!(AKEYCODE_VOLUME_DOWN);
+ insert_keycode!(AKEYCODE_POWER);
+ insert_keycode!(AKEYCODE_CAMERA);
+ insert_keycode!(AKEYCODE_CLEAR);
+ insert_keycode!(AKEYCODE_A);
+ insert_keycode!(AKEYCODE_B);
+ insert_keycode!(AKEYCODE_C);
+ insert_keycode!(AKEYCODE_D);
+ insert_keycode!(AKEYCODE_E);
+ insert_keycode!(AKEYCODE_F);
+ insert_keycode!(AKEYCODE_G);
+ insert_keycode!(AKEYCODE_H);
+ insert_keycode!(AKEYCODE_I);
+ insert_keycode!(AKEYCODE_J);
+ insert_keycode!(AKEYCODE_K);
+ insert_keycode!(AKEYCODE_L);
+ insert_keycode!(AKEYCODE_M);
+ insert_keycode!(AKEYCODE_N);
+ insert_keycode!(AKEYCODE_O);
+ insert_keycode!(AKEYCODE_P);
+ insert_keycode!(AKEYCODE_Q);
+ insert_keycode!(AKEYCODE_R);
+ insert_keycode!(AKEYCODE_S);
+ insert_keycode!(AKEYCODE_T);
+ insert_keycode!(AKEYCODE_U);
+ insert_keycode!(AKEYCODE_V);
+ insert_keycode!(AKEYCODE_W);
+ insert_keycode!(AKEYCODE_X);
+ insert_keycode!(AKEYCODE_Y);
+ insert_keycode!(AKEYCODE_Z);
+ insert_keycode!(AKEYCODE_COMMA);
+ insert_keycode!(AKEYCODE_PERIOD);
+ insert_keycode!(AKEYCODE_ALT_LEFT);
+ insert_keycode!(AKEYCODE_ALT_RIGHT);
+ insert_keycode!(AKEYCODE_SHIFT_LEFT);
+ insert_keycode!(AKEYCODE_SHIFT_RIGHT);
+ insert_keycode!(AKEYCODE_TAB);
+ insert_keycode!(AKEYCODE_SPACE);
+ insert_keycode!(AKEYCODE_SYM);
+ insert_keycode!(AKEYCODE_EXPLORER);
+ insert_keycode!(AKEYCODE_ENVELOPE);
+ insert_keycode!(AKEYCODE_ENTER);
+ insert_keycode!(AKEYCODE_DEL);
+ insert_keycode!(AKEYCODE_GRAVE);
+ insert_keycode!(AKEYCODE_MINUS);
+ insert_keycode!(AKEYCODE_EQUALS);
+ insert_keycode!(AKEYCODE_LEFT_BRACKET);
+ insert_keycode!(AKEYCODE_RIGHT_BRACKET);
+ insert_keycode!(AKEYCODE_BACKSLASH);
+ insert_keycode!(AKEYCODE_SEMICOLON);
+ insert_keycode!(AKEYCODE_APOSTROPHE);
+ insert_keycode!(AKEYCODE_SLASH);
+ insert_keycode!(AKEYCODE_AT);
+ insert_keycode!(AKEYCODE_NUM);
+ insert_keycode!(AKEYCODE_HEADSETHOOK);
+ insert_keycode!(AKEYCODE_FOCUS);
+ insert_keycode!(AKEYCODE_PLUS);
+ insert_keycode!(AKEYCODE_MENU);
+ insert_keycode!(AKEYCODE_NOTIFICATION);
+ insert_keycode!(AKEYCODE_SEARCH);
+ insert_keycode!(AKEYCODE_MEDIA_PLAY_PAUSE);
+ insert_keycode!(AKEYCODE_MEDIA_STOP);
+ insert_keycode!(AKEYCODE_MEDIA_NEXT);
+ insert_keycode!(AKEYCODE_MEDIA_PREVIOUS);
+ insert_keycode!(AKEYCODE_MEDIA_REWIND);
+ insert_keycode!(AKEYCODE_MEDIA_FAST_FORWARD);
+ insert_keycode!(AKEYCODE_MUTE);
+ insert_keycode!(AKEYCODE_PAGE_UP);
+ insert_keycode!(AKEYCODE_PAGE_DOWN);
+ insert_keycode!(AKEYCODE_PICTSYMBOLS);
+ insert_keycode!(AKEYCODE_SWITCH_CHARSET);
+ insert_keycode!(AKEYCODE_BUTTON_A);
+ insert_keycode!(AKEYCODE_BUTTON_B);
+ insert_keycode!(AKEYCODE_BUTTON_C);
+ insert_keycode!(AKEYCODE_BUTTON_X);
+ insert_keycode!(AKEYCODE_BUTTON_Y);
+ insert_keycode!(AKEYCODE_BUTTON_Z);
+ insert_keycode!(AKEYCODE_BUTTON_L1);
+ insert_keycode!(AKEYCODE_BUTTON_R1);
+ insert_keycode!(AKEYCODE_BUTTON_L2);
+ insert_keycode!(AKEYCODE_BUTTON_R2);
+ insert_keycode!(AKEYCODE_BUTTON_THUMBL);
+ insert_keycode!(AKEYCODE_BUTTON_THUMBR);
+ insert_keycode!(AKEYCODE_BUTTON_START);
+ insert_keycode!(AKEYCODE_BUTTON_SELECT);
+ insert_keycode!(AKEYCODE_BUTTON_MODE);
+ insert_keycode!(AKEYCODE_ESCAPE);
+ insert_keycode!(AKEYCODE_FORWARD_DEL);
+ insert_keycode!(AKEYCODE_CTRL_LEFT);
+ insert_keycode!(AKEYCODE_CTRL_RIGHT);
+ insert_keycode!(AKEYCODE_CAPS_LOCK);
+ insert_keycode!(AKEYCODE_SCROLL_LOCK);
+ insert_keycode!(AKEYCODE_META_LEFT);
+ insert_keycode!(AKEYCODE_META_RIGHT);
+ insert_keycode!(AKEYCODE_FUNCTION);
+ insert_keycode!(AKEYCODE_SYSRQ);
+ insert_keycode!(AKEYCODE_BREAK);
+ insert_keycode!(AKEYCODE_MOVE_HOME);
+ insert_keycode!(AKEYCODE_MOVE_END);
+ insert_keycode!(AKEYCODE_INSERT);
+ insert_keycode!(AKEYCODE_FORWARD);
+ insert_keycode!(AKEYCODE_MEDIA_PLAY);
+ insert_keycode!(AKEYCODE_MEDIA_PAUSE);
+ insert_keycode!(AKEYCODE_MEDIA_CLOSE);
+ insert_keycode!(AKEYCODE_MEDIA_EJECT);
+ insert_keycode!(AKEYCODE_MEDIA_RECORD);
+ insert_keycode!(AKEYCODE_F1);
+ insert_keycode!(AKEYCODE_F2);
+ insert_keycode!(AKEYCODE_F3);
+ insert_keycode!(AKEYCODE_F4);
+ insert_keycode!(AKEYCODE_F5);
+ insert_keycode!(AKEYCODE_F6);
+ insert_keycode!(AKEYCODE_F7);
+ insert_keycode!(AKEYCODE_F8);
+ insert_keycode!(AKEYCODE_F9);
+ insert_keycode!(AKEYCODE_F10);
+ insert_keycode!(AKEYCODE_F11);
+ insert_keycode!(AKEYCODE_F12);
+ insert_keycode!(AKEYCODE_NUM_LOCK);
+ insert_keycode!(AKEYCODE_NUMPAD_0);
+ insert_keycode!(AKEYCODE_NUMPAD_1);
+ insert_keycode!(AKEYCODE_NUMPAD_2);
+ insert_keycode!(AKEYCODE_NUMPAD_3);
+ insert_keycode!(AKEYCODE_NUMPAD_4);
+ insert_keycode!(AKEYCODE_NUMPAD_5);
+ insert_keycode!(AKEYCODE_NUMPAD_6);
+ insert_keycode!(AKEYCODE_NUMPAD_7);
+ insert_keycode!(AKEYCODE_NUMPAD_8);
+ insert_keycode!(AKEYCODE_NUMPAD_9);
+ insert_keycode!(AKEYCODE_NUMPAD_DIVIDE);
+ insert_keycode!(AKEYCODE_NUMPAD_MULTIPLY);
+ insert_keycode!(AKEYCODE_NUMPAD_SUBTRACT);
+ insert_keycode!(AKEYCODE_NUMPAD_ADD);
+ insert_keycode!(AKEYCODE_NUMPAD_DOT);
+ insert_keycode!(AKEYCODE_NUMPAD_COMMA);
+ insert_keycode!(AKEYCODE_NUMPAD_ENTER);
+ insert_keycode!(AKEYCODE_NUMPAD_EQUALS);
+ insert_keycode!(AKEYCODE_NUMPAD_LEFT_PAREN);
+ insert_keycode!(AKEYCODE_NUMPAD_RIGHT_PAREN);
+ insert_keycode!(AKEYCODE_VOLUME_MUTE);
+ insert_keycode!(AKEYCODE_INFO);
+ insert_keycode!(AKEYCODE_CHANNEL_UP);
+ insert_keycode!(AKEYCODE_CHANNEL_DOWN);
+ insert_keycode!(AKEYCODE_ZOOM_IN);
+ insert_keycode!(AKEYCODE_ZOOM_OUT);
+ insert_keycode!(AKEYCODE_TV);
+ insert_keycode!(AKEYCODE_WINDOW);
+ insert_keycode!(AKEYCODE_GUIDE);
+ insert_keycode!(AKEYCODE_DVR);
+ insert_keycode!(AKEYCODE_BOOKMARK);
+ insert_keycode!(AKEYCODE_CAPTIONS);
+ insert_keycode!(AKEYCODE_SETTINGS);
+ insert_keycode!(AKEYCODE_TV_POWER);
+ insert_keycode!(AKEYCODE_TV_INPUT);
+ insert_keycode!(AKEYCODE_STB_POWER);
+ insert_keycode!(AKEYCODE_STB_INPUT);
+ insert_keycode!(AKEYCODE_AVR_POWER);
+ insert_keycode!(AKEYCODE_AVR_INPUT);
+ insert_keycode!(AKEYCODE_PROG_RED);
+ insert_keycode!(AKEYCODE_PROG_GREEN);
+ insert_keycode!(AKEYCODE_PROG_YELLOW);
+ insert_keycode!(AKEYCODE_PROG_BLUE);
+ insert_keycode!(AKEYCODE_APP_SWITCH);
+ insert_keycode!(AKEYCODE_BUTTON_1);
+ insert_keycode!(AKEYCODE_BUTTON_2);
+ insert_keycode!(AKEYCODE_BUTTON_3);
+ insert_keycode!(AKEYCODE_BUTTON_4);
+ insert_keycode!(AKEYCODE_BUTTON_5);
+ insert_keycode!(AKEYCODE_BUTTON_6);
+ insert_keycode!(AKEYCODE_BUTTON_7);
+ insert_keycode!(AKEYCODE_BUTTON_8);
+ insert_keycode!(AKEYCODE_BUTTON_9);
+ insert_keycode!(AKEYCODE_BUTTON_10);
+ insert_keycode!(AKEYCODE_BUTTON_11);
+ insert_keycode!(AKEYCODE_BUTTON_12);
+ insert_keycode!(AKEYCODE_BUTTON_13);
+ insert_keycode!(AKEYCODE_BUTTON_14);
+ insert_keycode!(AKEYCODE_BUTTON_15);
+ insert_keycode!(AKEYCODE_BUTTON_16);
+ insert_keycode!(AKEYCODE_LANGUAGE_SWITCH);
+ insert_keycode!(AKEYCODE_MANNER_MODE);
+ insert_keycode!(AKEYCODE_3D_MODE);
+ insert_keycode!(AKEYCODE_CONTACTS);
+ insert_keycode!(AKEYCODE_CALENDAR);
+ insert_keycode!(AKEYCODE_MUSIC);
+ insert_keycode!(AKEYCODE_CALCULATOR);
+ insert_keycode!(AKEYCODE_ZENKAKU_HANKAKU);
+ insert_keycode!(AKEYCODE_EISU);
+ insert_keycode!(AKEYCODE_MUHENKAN);
+ insert_keycode!(AKEYCODE_HENKAN);
+ insert_keycode!(AKEYCODE_KATAKANA_HIRAGANA);
+ insert_keycode!(AKEYCODE_YEN);
+ insert_keycode!(AKEYCODE_RO);
+ insert_keycode!(AKEYCODE_KANA);
+ insert_keycode!(AKEYCODE_ASSIST);
+ insert_keycode!(AKEYCODE_BRIGHTNESS_DOWN);
+ insert_keycode!(AKEYCODE_BRIGHTNESS_UP);
+ insert_keycode!(AKEYCODE_MEDIA_AUDIO_TRACK);
+ insert_keycode!(AKEYCODE_SLEEP);
+ insert_keycode!(AKEYCODE_WAKEUP);
+ insert_keycode!(AKEYCODE_PAIRING);
+ insert_keycode!(AKEYCODE_MEDIA_TOP_MENU);
+ insert_keycode!(AKEYCODE_LAST_CHANNEL);
+ insert_keycode!(AKEYCODE_TV_DATA_SERVICE);
+ insert_keycode!(AKEYCODE_VOICE_ASSIST);
+ insert_keycode!(AKEYCODE_TV_RADIO_SERVICE);
+ insert_keycode!(AKEYCODE_TV_TELETEXT);
+ insert_keycode!(AKEYCODE_TV_NUMBER_ENTRY);
+ insert_keycode!(AKEYCODE_TV_TERRESTRIAL_ANALOG);
+ insert_keycode!(AKEYCODE_TV_TERRESTRIAL_DIGITAL);
+ insert_keycode!(AKEYCODE_TV_SATELLITE);
+ insert_keycode!(AKEYCODE_TV_SATELLITE_BS);
+ insert_keycode!(AKEYCODE_TV_SATELLITE_CS);
+ insert_keycode!(AKEYCODE_TV_SATELLITE_SERVICE);
+ insert_keycode!(AKEYCODE_TV_NETWORK);
+ insert_keycode!(AKEYCODE_TV_ANTENNA_CABLE);
+ insert_keycode!(AKEYCODE_TV_INPUT_HDMI_1);
+ insert_keycode!(AKEYCODE_TV_INPUT_HDMI_2);
+ insert_keycode!(AKEYCODE_TV_INPUT_HDMI_3);
+ insert_keycode!(AKEYCODE_TV_INPUT_HDMI_4);
+ insert_keycode!(AKEYCODE_TV_INPUT_COMPOSITE_1);
+ insert_keycode!(AKEYCODE_TV_INPUT_COMPOSITE_2);
+ insert_keycode!(AKEYCODE_TV_INPUT_COMPONENT_1);
+ insert_keycode!(AKEYCODE_TV_INPUT_COMPONENT_2);
+ insert_keycode!(AKEYCODE_TV_INPUT_VGA_1);
+ insert_keycode!(AKEYCODE_TV_AUDIO_DESCRIPTION);
+ insert_keycode!(AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP);
+ insert_keycode!(AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN);
+ insert_keycode!(AKEYCODE_TV_ZOOM_MODE);
+ insert_keycode!(AKEYCODE_TV_CONTENTS_MENU);
+ insert_keycode!(AKEYCODE_TV_MEDIA_CONTEXT_MENU);
+ insert_keycode!(AKEYCODE_TV_TIMER_PROGRAMMING);
+ insert_keycode!(AKEYCODE_HELP);
+ insert_keycode!(AKEYCODE_NAVIGATE_PREVIOUS);
+ insert_keycode!(AKEYCODE_NAVIGATE_NEXT);
+ insert_keycode!(AKEYCODE_NAVIGATE_IN);
+ insert_keycode!(AKEYCODE_NAVIGATE_OUT);
+ insert_keycode!(AKEYCODE_STEM_PRIMARY);
+ insert_keycode!(AKEYCODE_STEM_1);
+ insert_keycode!(AKEYCODE_STEM_2);
+ insert_keycode!(AKEYCODE_STEM_3);
+ insert_keycode!(AKEYCODE_DPAD_UP_LEFT);
+ insert_keycode!(AKEYCODE_DPAD_DOWN_LEFT);
+ insert_keycode!(AKEYCODE_DPAD_UP_RIGHT);
+ insert_keycode!(AKEYCODE_DPAD_DOWN_RIGHT);
+ insert_keycode!(AKEYCODE_MEDIA_SKIP_FORWARD);
+ insert_keycode!(AKEYCODE_MEDIA_SKIP_BACKWARD);
+ insert_keycode!(AKEYCODE_MEDIA_STEP_FORWARD);
+ insert_keycode!(AKEYCODE_MEDIA_STEP_BACKWARD);
+ insert_keycode!(AKEYCODE_SOFT_SLEEP);
+ insert_keycode!(AKEYCODE_CUT);
+ insert_keycode!(AKEYCODE_COPY);
+ insert_keycode!(AKEYCODE_PASTE);
+ insert_keycode!(AKEYCODE_SYSTEM_NAVIGATION_UP);
+ insert_keycode!(AKEYCODE_SYSTEM_NAVIGATION_DOWN);
+ insert_keycode!(AKEYCODE_SYSTEM_NAVIGATION_LEFT);
+ insert_keycode!(AKEYCODE_SYSTEM_NAVIGATION_RIGHT);
+ insert_keycode!(AKEYCODE_ALL_APPS);
+ insert_keycode!(AKEYCODE_REFRESH);
+ insert_keycode!(AKEYCODE_THUMBS_UP);
+ insert_keycode!(AKEYCODE_THUMBS_DOWN);
+ insert_keycode!(AKEYCODE_PROFILE_SWITCH);
+ insert_keycode!(AKEYCODE_VIDEO_APP_1);
+ insert_keycode!(AKEYCODE_VIDEO_APP_2);
+ insert_keycode!(AKEYCODE_VIDEO_APP_3);
+ insert_keycode!(AKEYCODE_VIDEO_APP_4);
+ insert_keycode!(AKEYCODE_VIDEO_APP_5);
+ insert_keycode!(AKEYCODE_VIDEO_APP_6);
+ insert_keycode!(AKEYCODE_VIDEO_APP_7);
+ insert_keycode!(AKEYCODE_VIDEO_APP_8);
+ insert_keycode!(AKEYCODE_FEATURED_APP_1);
+ insert_keycode!(AKEYCODE_FEATURED_APP_2);
+ insert_keycode!(AKEYCODE_FEATURED_APP_3);
+ insert_keycode!(AKEYCODE_FEATURED_APP_4);
+ insert_keycode!(AKEYCODE_DEMO_APP_1);
+ insert_keycode!(AKEYCODE_DEMO_APP_2);
+ insert_keycode!(AKEYCODE_DEMO_APP_3);
+ insert_keycode!(AKEYCODE_DEMO_APP_4);
+ insert_keycode!(AKEYCODE_KEYBOARD_BACKLIGHT_DOWN);
+ insert_keycode!(AKEYCODE_KEYBOARD_BACKLIGHT_UP);
+ insert_keycode!(AKEYCODE_KEYBOARD_BACKLIGHT_TOGGLE);
+ insert_keycode!(AKEYCODE_STYLUS_BUTTON_PRIMARY);
+ insert_keycode!(AKEYCODE_STYLUS_BUTTON_SECONDARY);
+ insert_keycode!(AKEYCODE_STYLUS_BUTTON_TERTIARY);
+ insert_keycode!(AKEYCODE_STYLUS_BUTTON_TAIL);
+ insert_keycode!(AKEYCODE_RECENT_APPS);
+ insert_keycode!(AKEYCODE_MACRO_1);
+ insert_keycode!(AKEYCODE_MACRO_2);
+ insert_keycode!(AKEYCODE_MACRO_3);
+ insert_keycode!(AKEYCODE_MACRO_4);
+
+ map
+}
+
+// Build the axes map
+fn build_axes_map() -> HashMap {
+ let mut map = HashMap::new();
+ macro_rules! insert_axis {
+ ($name:ident) => {
+ let (name, value) = define_axis!($name);
+ map.insert(name, value);
+ };
+ }
+ insert_axis!(AMOTION_EVENT_AXIS_X);
+ insert_axis!(AMOTION_EVENT_AXIS_Y);
+ insert_axis!(AMOTION_EVENT_AXIS_PRESSURE);
+ insert_axis!(AMOTION_EVENT_AXIS_SIZE);
+ insert_axis!(AMOTION_EVENT_AXIS_TOUCH_MAJOR);
+ insert_axis!(AMOTION_EVENT_AXIS_TOUCH_MINOR);
+ insert_axis!(AMOTION_EVENT_AXIS_TOOL_MAJOR);
+ insert_axis!(AMOTION_EVENT_AXIS_TOOL_MINOR);
+ insert_axis!(AMOTION_EVENT_AXIS_ORIENTATION);
+ insert_axis!(AMOTION_EVENT_AXIS_VSCROLL);
+ insert_axis!(AMOTION_EVENT_AXIS_HSCROLL);
+ insert_axis!(AMOTION_EVENT_AXIS_Z);
+ insert_axis!(AMOTION_EVENT_AXIS_RX);
+ insert_axis!(AMOTION_EVENT_AXIS_RY);
+ insert_axis!(AMOTION_EVENT_AXIS_RZ);
+ insert_axis!(AMOTION_EVENT_AXIS_HAT_X);
+ insert_axis!(AMOTION_EVENT_AXIS_HAT_Y);
+ insert_axis!(AMOTION_EVENT_AXIS_LTRIGGER);
+ insert_axis!(AMOTION_EVENT_AXIS_RTRIGGER);
+ insert_axis!(AMOTION_EVENT_AXIS_THROTTLE);
+ insert_axis!(AMOTION_EVENT_AXIS_RUDDER);
+ insert_axis!(AMOTION_EVENT_AXIS_WHEEL);
+ insert_axis!(AMOTION_EVENT_AXIS_GAS);
+ insert_axis!(AMOTION_EVENT_AXIS_BRAKE);
+ insert_axis!(AMOTION_EVENT_AXIS_DISTANCE);
+ insert_axis!(AMOTION_EVENT_AXIS_TILT);
+ insert_axis!(AMOTION_EVENT_AXIS_SCROLL);
+ insert_axis!(AMOTION_EVENT_AXIS_RELATIVE_X);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_1);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_2);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_3);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_4);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_5);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_6);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_7);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_8);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_9);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_10);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_11);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_12);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_13);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_14);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_15);
+ insert_axis!(AMOTION_EVENT_AXIS_GENERIC_16);
+ insert_axis!(AMOTION_EVENT_AXIS_GESTURE_X_OFFSET);
+ insert_axis!(AMOTION_EVENT_AXIS_GESTURE_Y_OFFSET);
+ insert_axis!(AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE);
+ insert_axis!(AMOTION_EVENT_AXIS_GESTURE_SCROLL_Y_DISTANCE);
+ insert_axis!(AMOTION_EVENT_AXIS_GESTURE_PINCH_SCALE_FACTOR);
+ insert_axis!(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT);
+ map
+}
+
+// Build the flags map
+fn build_flags_map() -> HashMap {
+ let mut map = HashMap::new();
+ macro_rules! insert_flag {
+ ($name:ident) => {
+ let (name, value) = define_flag!($name);
+ map.insert(name, value);
+ };
+ }
+ insert_flag!(POLICY_FLAG_VIRTUAL);
+ insert_flag!(POLICY_FLAG_FUNCTION);
+ insert_flag!(POLICY_FLAG_GESTURE);
+ insert_flag!(POLICY_FLAG_WAKE);
+ insert_flag!(POLICY_FLAG_FALLBACK_USAGE_MAPPING);
+ map
+}
+
+// Static lookup tables (lazily initialized)
+static KEYCODES: OnceLock> = OnceLock::new();
+static AXES: OnceLock> = OnceLock::new();
+static FLAGS: OnceLock> = OnceLock::new();
+
+fn get_keycodes() -> &'static HashMap {
+ KEYCODES.get_or_init(build_keycodes_map)
+}
+
+fn get_axes() -> &'static HashMap {
+ AXES.get_or_init(build_axes_map)
+}
+
+fn get_flags() -> &'static HashMap {
+ FLAGS.get_or_init(build_flags_map)
+}
+
+/// Look up a key code by its label.
+pub fn get_key_code_by_label(label: &str) -> Option {
+ get_keycodes().get(&format!("AKEYCODE_{}", label)).copied()
+}
+
+/// Look up an axis by its label.
+pub fn get_axis_by_label(label: &str) -> Option {
+ get_axes()
+ .get(&format!("AMOTION_EVENT_AXIS_{}", label))
+ .copied()
+}
+
+/// Look up a key flag by its label.
+pub fn get_key_flag_by_label(label: &str) -> Option {
+ get_flags().get(&format!("POLICY_FLAG_{}", label)).copied()
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_file_finder.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_file_finder.rs
new file mode 100644
index 0000000000..52c1c9e8c3
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_file_finder.rs
@@ -0,0 +1,56 @@
+use std::{
+ fs::{self, File},
+ io::ErrorKind,
+ path::PathBuf,
+};
+
+/// Trait for finding key layout files.
+/// This allows dependency injection for testing purposes.
+pub trait KeyLayoutFileFinder: Send + Sync {
+ /// Find a key layout file in the system by its name.
+ fn find_system_key_layout_file_by_name(&self, name: &str) -> Option;
+
+ /// Find a key layout file shipped with Key Mapper by its name.
+ fn find_key_mapper_key_layout_file_by_name(&self, name: &str) -> Option;
+}
+
+/// Default implementation that uses the real file system.
+/// This searches the standard Android key layout file locations.
+pub struct AndroidKeyLayoutFileFinder;
+
+impl KeyLayoutFileFinder for AndroidKeyLayoutFileFinder {
+ fn find_system_key_layout_file_by_name(&self, name: &str) -> Option {
+ // See https://source.android.com/docs/core/interaction/input/key-layout-files#location
+ let path_prefixes = vec![
+ "/odm/usr/".to_string(),
+ "/vendor/usr/".to_string(),
+ "/system/usr/".to_string(),
+ "/data/system/devices/".to_string(),
+ ];
+
+ for prefix in &path_prefixes {
+ let path = PathBuf::new()
+ .join(prefix)
+ .join("keylayout")
+ .join(format!("{}.kl", name));
+
+ match fs::metadata(&path) {
+ Ok(metadata) if metadata.is_file() => {
+ if File::open(&path).is_ok() {
+ return Some(path);
+ }
+ }
+ Err(e) if e.kind() != ErrorKind::NotFound => {
+ debug!("Error accessing {:?}: {}", path, e);
+ }
+ _ => {}
+ }
+ }
+
+ None
+ }
+
+ fn find_key_mapper_key_layout_file_by_name(&self, _name: &str) -> Option {
+ None
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_map.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_map.rs
new file mode 100644
index 0000000000..cc2be6cb88
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_map.rs
@@ -0,0 +1,403 @@
+//! Key layout map parser for Android key layout files.
+//!
+//! This module provides functionality to parse Android key layout (.kl) files
+//! and map scan codes to Android key codes.
+//!
+//! AOSP keylayout files can be found at:
+//! https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/data/keyboards/
+
+use crate::android::keylayout::input_event_lookup::{get_axis_by_label, get_key_code_by_label};
+use crate::android::keylayout::tokenizer::Tokenizer;
+use std::collections::HashMap;
+use std::fmt;
+use std::path::PathBuf;
+
+/// Describes a mapping from keyboard scan codes to Android key codes.
+///
+/// This object is immutable after it has been loaded.
+pub struct KeyLayoutMap {
+ pub scan_codes: Vec,
+ /// Maps scan codes to key codes.
+ keys_by_scan_code: HashMap,
+ /// Maps key codes to their corresponding scan codes (reverse lookup).
+ scan_codes_by_key_code: HashMap,
+ axes: HashMap,
+}
+
+/// Represents axis information for joystick/gamepad axes.
+#[derive(Debug, Clone)]
+pub struct KeyLayoutAxisInfo {
+ pub mode: KeyLayoutAxisMode,
+ pub axis: u32,
+ pub high_axis: Option,
+ pub split_value: Option,
+ pub flat_override: Option,
+}
+
+/// Axis mapping mode.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum KeyLayoutAxisMode {
+ /// Axis value is reported directly.
+ Normal,
+ /// Axis value should be inverted before reporting.
+ Invert,
+ /// Axis value should be split into two axes.
+ Split,
+}
+
+const WHITESPACE: &str = " \t\r";
+
+impl KeyLayoutMap {
+ /// Load a key layout map from a file path.
+ pub fn load_from_file(file_path: PathBuf) -> Result {
+ let tokenizer = Tokenizer::from_file(file_path)?;
+ Self::load(tokenizer)
+ }
+
+ /// Load a key layout map from file contents (useful for testing).
+ pub fn load_from_contents(contents: &str) -> Result {
+ let tokenizer = Tokenizer::from_contents(PathBuf::new(), contents);
+ Self::load(tokenizer)
+ }
+
+ fn load(mut tokenizer: Tokenizer) -> Result {
+ let mut map = KeyLayoutMap {
+ scan_codes: Vec::with_capacity(16),
+ keys_by_scan_code: HashMap::new(),
+ scan_codes_by_key_code: HashMap::new(),
+ axes: HashMap::new(),
+ };
+
+ let mut parser = Parser::new(&mut map, &mut tokenizer);
+ parser
+ .parse()
+ .inspect_err(|err| error!("Failed to parse key layout map: {:?}", err))?;
+
+ Ok(map)
+ }
+
+ /// Map a scan code to an Android key code.
+ ///
+ /// Returns `Some(key_code)` on success, or `None` if not found.
+ pub fn map_key(&self, scan_code: u32) -> Option {
+ self.keys_by_scan_code.get(&scan_code).copied()
+ }
+
+ /// Map a scan code to axis information.
+ ///
+ /// Returns `Some(axis_info)` if the scan code maps to an axis, or `None` if not found.
+ pub fn map_axis(&self, scan_code: u32) -> Option {
+ self.axes.get(&scan_code).cloned()
+ }
+
+ /// Find all scan codes that map to the given key code.
+ pub fn find_scan_code_for_key(&self, key_code: u32) -> Option {
+ self.scan_codes_by_key_code.get(&key_code).cloned()
+ }
+}
+
+impl fmt::Debug for KeyLayoutMap {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut debug_struct = f.debug_struct("KeyLayoutMap");
+ debug_struct.field("keys_count", &self.keys_by_scan_code.len());
+ debug_struct.field("axes_count", &self.axes.len());
+
+ // Show a sample of keys (first 5) if there are any
+ if !self.keys_by_scan_code.is_empty() {
+ let sample_keys: Vec<_> = self
+ .keys_by_scan_code
+ .iter()
+ .take(5)
+ .map(|(scan_code, key_code)| (*scan_code, *key_code))
+ .collect();
+ debug_struct.field("sample_keys", &sample_keys);
+ }
+
+ // Show a sample of axes (first 5) if there are any
+ if !self.axes.is_empty() {
+ let sample_axes: Vec<_> = self.axes.iter().take(5).collect();
+ debug_struct.field("sample_axes", &sample_axes);
+ }
+
+ debug_struct.finish()
+ }
+}
+
+/// Parser for key layout map files.
+struct Parser<'a> {
+ map: &'a mut KeyLayoutMap,
+ tokenizer: &'a mut Tokenizer,
+}
+
+impl<'a> Parser<'a> {
+ fn new(map: &'a mut KeyLayoutMap, tokenizer: &'a mut Tokenizer) -> Self {
+ Self { map, tokenizer }
+ }
+
+ fn parse(&mut self) -> Result<(), String> {
+ while !self.tokenizer.is_eof() {
+ self.tokenizer.skip_delimiters(WHITESPACE);
+
+ if !self.tokenizer.is_eol() && self.tokenizer.peek_char() != '#' {
+ let keyword_token = self.tokenizer.next_token(WHITESPACE);
+ let mut skipped_line: bool = false;
+
+ match keyword_token.as_str() {
+ "key" => {
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ let code_token = self.tokenizer.next_token(WHITESPACE);
+
+ // Skip "usage" entries - we only support scan codes
+ if code_token == "usage" {
+ skipped_line = true;
+ } else {
+ self.parse_key(&code_token)?;
+ }
+ }
+ "axis" => {
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ self.parse_axis()?;
+ }
+ "led" | "sensor" => {
+ // Skip LEDs and sensors, we don't need them
+ self.tokenizer.next_line();
+ continue;
+ }
+ "requires_kernel_config" => {
+ self.tokenizer.next_line();
+ continue;
+ }
+ "usage" => {
+ // Skip usage code entries - evdev only provides scan codes
+ self.tokenizer.next_line();
+ continue;
+ }
+ _ => {
+ return Err(format!(
+ "{}: Expected keyword, got '{}'.",
+ self.tokenizer.get_location(),
+ keyword_token
+ ));
+ }
+ }
+
+ self.tokenizer.skip_delimiters(WHITESPACE);
+
+ if !skipped_line && !self.tokenizer.is_eol() && self.tokenizer.peek_char() != '#' {
+ return Err(format!(
+ "{}: Expected end of line or trailing comment, got '{}'.",
+ self.tokenizer.get_location(),
+ self.tokenizer.peek_remainder_of_line()
+ ));
+ }
+ }
+
+ self.tokenizer.next_line();
+ }
+
+ Ok(())
+ }
+
+ fn parse_key(&mut self, code_token: &str) -> Result<(), String> {
+ let scan_code = parse_int(code_token).ok_or_else(|| {
+ format!(
+ "{}: Expected key scan code number, got '{}'.",
+ self.tokenizer.get_location(),
+ code_token
+ )
+ })?;
+
+ if scan_code < 0 {
+ return Err(format!(
+ "{} is not a valid key scan code. Negative numbers are not allowed.",
+ scan_code
+ ));
+ }
+
+ if self.map.keys_by_scan_code.contains_key(&(scan_code as u32)) {
+ return Err(format!(
+ "{}: Duplicate entry for key scan code '{}'.",
+ self.tokenizer.get_location(),
+ code_token
+ ));
+ }
+
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ let key_code_token = self.tokenizer.next_token(WHITESPACE);
+ let key_code = get_key_code_by_label(&key_code_token);
+
+ // Skip any remaining tokens on the line (flags, etc.)
+ loop {
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ if self.tokenizer.is_eol() || self.tokenizer.peek_char() == '#' {
+ break;
+ }
+ // Consume and skip the token (flags are ignored)
+ self.tokenizer.next_token(WHITESPACE);
+ }
+
+ // Only insert if the key code is known
+ if let Some(key_code) = key_code {
+ self.map.scan_codes.push(scan_code as u32);
+ self.map
+ .keys_by_scan_code
+ .insert(scan_code as u32, key_code);
+
+ // Only insert if this key_code doesn't already have a scan code.
+ // This ensures we keep the first (typically non-FUNCTION) scan code.
+ self.map
+ .scan_codes_by_key_code
+ .entry(key_code)
+ .or_insert(scan_code as u32);
+ }
+
+ Ok(())
+ }
+
+ fn parse_axis(&mut self) -> Result<(), String> {
+ let scan_code_token = self.tokenizer.next_token(WHITESPACE);
+ let scan_code = parse_int(&scan_code_token).ok_or_else(|| {
+ format!(
+ "{}: Expected axis scan code number, got '{}'.",
+ self.tokenizer.get_location(),
+ scan_code_token
+ )
+ })?;
+
+ if scan_code < 0 {
+ return Err(format!(
+ "{} is not a valid key scan code for an axis. Negative numbers are not allowed.",
+ scan_code
+ ));
+ }
+
+ if self.map.axes.contains_key(&(scan_code as u32)) {
+ return Err(format!(
+ "{}: Duplicate entry for axis scan code '{}'.",
+ self.tokenizer.get_location(),
+ scan_code_token
+ ));
+ }
+
+ let mut axis_mode: KeyLayoutAxisMode = KeyLayoutAxisMode::Normal;
+ let axis: u32;
+ let mut split_value: Option = None;
+ let mut high_axis: Option = None;
+ let mut flat_override: Option = None;
+
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ let token = self.tokenizer.next_token(WHITESPACE);
+
+ if token == "invert" {
+ axis_mode = KeyLayoutAxisMode::Invert;
+
+ self.tokenizer.skip_delimiters(WHITESPACE);
+
+ let axis_token = self.tokenizer.next_token(WHITESPACE);
+
+ axis = get_axis_by_label(&axis_token).ok_or_else(|| {
+ format!(
+ "{}: Expected inverted axis label, got '{}'.",
+ self.tokenizer.get_location(),
+ axis_token
+ )
+ })?;
+ } else if token == "split" {
+ axis_mode = KeyLayoutAxisMode::Split;
+
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ let split_token = self.tokenizer.next_token(WHITESPACE);
+ let split_value_raw = parse_int(&split_token).ok_or_else(|| {
+ format!(
+ "{}: Expected split value, got '{}'.",
+ self.tokenizer.get_location(),
+ split_token
+ )
+ })?;
+ split_value = Some(split_value_raw);
+
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ let low_axis_token = self.tokenizer.next_token(WHITESPACE);
+ axis = get_axis_by_label(&low_axis_token).ok_or_else(|| {
+ format!(
+ "{}: Expected low axis label, got '{}'.",
+ self.tokenizer.get_location(),
+ low_axis_token
+ )
+ })?;
+
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ let high_axis_token = self.tokenizer.next_token(WHITESPACE);
+ let high_axis_raw = get_axis_by_label(&high_axis_token).ok_or_else(|| {
+ format!(
+ "{}: Expected high axis label, got '{}'.",
+ self.tokenizer.get_location(),
+ high_axis_token
+ )
+ })?;
+ high_axis = Some(high_axis_raw);
+ } else {
+ axis = get_axis_by_label(&token).ok_or_else(|| {
+ format!(
+ "{}: Expected axis label, 'split' or 'invert', got '{}'.",
+ self.tokenizer.get_location(),
+ token
+ )
+ })?;
+ }
+
+ loop {
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ if self.tokenizer.is_eol() || self.tokenizer.peek_char() == '#' {
+ break;
+ }
+
+ let keyword_token = self.tokenizer.next_token(WHITESPACE);
+ if keyword_token == "flat" {
+ self.tokenizer.skip_delimiters(WHITESPACE);
+ let flat_token = self.tokenizer.next_token(WHITESPACE);
+ let flat_override_raw = parse_int(&flat_token).ok_or_else(|| {
+ format!(
+ "{}: Expected flat value, got '{}'.",
+ self.tokenizer.get_location(),
+ flat_token
+ )
+ })?;
+ flat_override = Some(flat_override_raw);
+ } else {
+ return Err(format!(
+ "{}: Expected keyword 'flat', got '{}'.",
+ self.tokenizer.get_location(),
+ keyword_token
+ ));
+ }
+ }
+
+ let axis_info: KeyLayoutAxisInfo = KeyLayoutAxisInfo {
+ mode: axis_mode,
+ axis,
+ high_axis,
+ split_value,
+ flat_override,
+ };
+
+ self.map.axes.insert(scan_code as u32, axis_info);
+ Ok(())
+ }
+}
+
+/// Parse an integer from a string (supports decimal, hex with 0x prefix, and octal with 0 prefix).
+pub fn parse_int(s: &str) -> Option {
+ if s.is_empty() {
+ return None;
+ }
+
+ // Handle hex (0x prefix) and octal (0 prefix)
+ if s.starts_with("0x") || s.starts_with("0X") {
+ i32::from_str_radix(&s[2..], 16).ok()
+ } else if s.starts_with('0') && s.len() > 1 {
+ i32::from_str_radix(&s[1..], 8).ok()
+ } else {
+ s.parse::().ok()
+ }
+}
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
new file mode 100644
index 0000000000..632c72d970
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/key_layout_map_manager.rs
@@ -0,0 +1,249 @@
+use crate::android::keylayout::generic_key_layout::GENERIC_KEY_LAYOUT_CONTENTS;
+use crate::android::keylayout::key_layout_file_finder::{
+ AndroidKeyLayoutFileFinder, KeyLayoutFileFinder,
+};
+use crate::android::keylayout::key_layout_map::KeyLayoutMap;
+use crate::evdev_device_info::EvdevDeviceInfo;
+use evdev::enums::{EventCode, EventType};
+use evdev::util::int_to_event_code;
+use libc::c_uint;
+use log::{error, info};
+use std::collections::HashMap;
+use std::error::Error;
+use std::path::PathBuf;
+use std::sync::{Arc, Mutex, OnceLock};
+
+static KEY_LAYOUT_MANAGER: OnceLock> = OnceLock::new();
+static GENERIC_KEY_LAYOUT_MAP: OnceLock> = OnceLock::new();
+
+/// Get the static generic KeyLayoutMap instance.
+/// This is lazily initialized from hardcoded key mappings based on AOSP Generic.kl.
+pub fn get_generic_key_layout_map() -> Arc {
+ Arc::clone(GENERIC_KEY_LAYOUT_MAP.get_or_init(|| {
+ Arc::new(
+ KeyLayoutMap::load_from_contents(GENERIC_KEY_LAYOUT_CONTENTS)
+ .expect("Failed to parse hardcoded Generic key layout"),
+ )
+ }))
+}
+
+/// Manages KeyLayoutMap caching and key code mapping
+/// This is the only file that directly interacts with KeyLayoutMap C bindings
+/// and the only file that finds key layout file paths
+pub struct KeyLayoutMapManager {
+ /// KeyLayoutMap cache
+ /// Maps device path to KeyLayoutMap handle. If the value is None then
+ /// the key layout map could not be found or there was an error parsing,
+ /// and it shouldn't be attempted again.
+ pub key_layout_maps: Mutex>>>,
+ /// File finder for locating key layout files
+ file_finder: Arc,
+}
+
+impl KeyLayoutMapManager {
+ pub fn get() -> Arc {
+ Arc::clone(KEY_LAYOUT_MANAGER.get_or_init(|| Arc::new(Self::new())))
+ }
+
+ fn new() -> Self {
+ Self {
+ key_layout_maps: Mutex::new(HashMap::with_capacity(32)),
+ file_finder: Arc::new(AndroidKeyLayoutFileFinder),
+ }
+ }
+
+ /// Create a new instance with a custom file finder.
+ /// This is primarily useful for testing.
+ pub fn with_file_finder(file_finder: Arc) -> Self {
+ Self {
+ key_layout_maps: Mutex::new(HashMap::with_capacity(32)),
+ file_finder,
+ }
+ }
+
+ /// Map a raw evdev key code to Android key code.
+ /// Returns the android keycode if the key is found in the device's map,
+ /// falling back to the generic key layout if not found.
+ pub fn map_key(
+ &self,
+ device_info: &EvdevDeviceInfo,
+ scan_code: u32,
+ ) -> Result
, Box> {
+ let device_map = self.get_key_layout_map_lazy(device_info)?;
+
+ if let Some(map) = device_map {
+ if let Some(key_code) = map.map_key(scan_code) {
+ return Ok(Some(key_code));
+ }
+ }
+
+ // Fall back to generic key layout
+ Ok(get_generic_key_layout_map().map_key(scan_code))
+ }
+
+ /// Find the scan code for a given Android key code.
+ /// Returns the scan code if found in the device's map,
+ /// falling back to the generic key layout if not found.
+ pub fn find_scan_code_for_key(
+ &self,
+ device_info: &EvdevDeviceInfo,
+ key_code: u32,
+ ) -> Result
, Box> {
+ let device_map = self.get_key_layout_map_lazy(device_info)?;
+
+ if let Some(map) = device_map {
+ if let Some(scan_code) = map.find_scan_code_for_key(key_code) {
+ return Ok(Some(scan_code));
+ }
+ }
+
+ // Fall back to generic key layout
+ Ok(get_generic_key_layout_map().find_scan_code_for_key(key_code))
+ }
+
+ pub fn preload_key_layout_map(
+ &self,
+ device_info: &EvdevDeviceInfo,
+ ) -> Result
>, Box> {
+ self.get_key_layout_map_lazy(device_info)
+ .inspect_err(|err| {
+ error!(
+ "Error preloading key layout map for device {}: {}",
+ device_info.name, err
+ )
+ })
+ }
+
+ /// Get or load a key layout map for the given device identifier.
+ /// This method is public for testing purposes.
+ fn get_key_layout_map_lazy(
+ &self,
+ device_info: &EvdevDeviceInfo,
+ ) -> Result
>, Box> {
+ let mut key_layout_maps = self.key_layout_maps.lock().unwrap();
+
+ if let Some(key_layout_map) = key_layout_maps.get(device_info) {
+ return Ok(key_layout_map.clone());
+ }
+
+ let key_layout_map_paths = self.find_key_layout_files(device_info);
+ info!(
+ "Found key layout map files for device {}: {:?}",
+ device_info.name, key_layout_map_paths
+ );
+
+ for path in key_layout_map_paths {
+ match KeyLayoutMap::load_from_file(path) {
+ Ok(key_layout_map) => {
+ let option = Some(Arc::new(key_layout_map));
+ key_layout_maps.insert(device_info.clone(), option.clone());
+ return Ok(option);
+ }
+ Err(e) => {
+ error!("Error parsing key layout map: {}", e);
+ // Continue to try the next file instead of failing immediately
+ }
+ }
+ }
+
+ // No key layout map files were found or parsed successfully.
+ // Fall back to the hardcoded generic key layout map.
+ info!(
+ "No key layout files found for device {}, using hardcoded Generic fallback",
+ device_info.name
+ );
+ let fallback = Some(get_generic_key_layout_map());
+ key_layout_maps.insert(device_info.clone(), fallback.clone());
+ Ok(fallback)
+ }
+
+ /// Find all the possible key layout files to use for a device ordered by their priority.
+ /// A list is returned so there are fallback key layout files if one can't be parsed
+ /// for whatever reason.
+ /// Tries multiple naming schemes based on vendor/product/version, then device name, then Generic.
+ /// It first tries searching the system for the file, and then does the search again
+ /// in the files shipped with Key Mapper.
+ ///
+ /// See https://source.android.com/docs/core/interaction/input/key-layout-files#location
+ pub fn find_key_layout_files(&self, device_info: &EvdevDeviceInfo) -> Vec {
+ let name = device_info.name.as_str();
+ let vendor = device_info.vendor;
+ let product = device_info.product;
+ let version = device_info.version;
+
+ let mut paths: Vec = Vec::new();
+
+ // Try vendor/product/version path first
+ if vendor != 0 && product != 0 {
+ if version != 0 {
+ let version_name = format!(
+ "Vendor_{:04x}_Product_{:04x}_Version_{:04x}",
+ vendor, product, version
+ );
+ if let Some(path) = self
+ .file_finder
+ .find_system_key_layout_file_by_name(&version_name)
+ {
+ paths.push(path);
+ }
+ }
+
+ // Try vendor/product
+ let product_name = format!("Vendor_{:04x}_Product_{:04x}", vendor, product);
+ if let Some(path) = self
+ .file_finder
+ .find_system_key_layout_file_by_name(&product_name)
+ {
+ paths.push(path);
+ }
+ }
+
+ // Try device name (canonical)
+ let canonical_name = get_canonical_name(name);
+ if let Some(path) = self
+ .file_finder
+ .find_system_key_layout_file_by_name(&canonical_name)
+ {
+ paths.push(path);
+ }
+
+ // Try system generic
+ if let Some(path) = self
+ .file_finder
+ .find_system_key_layout_file_by_name("Generic")
+ {
+ paths.push(path);
+ }
+
+ paths
+ }
+
+ pub fn map_key_codes_to_event_codes(key_codes: &[u32]) -> Vec {
+ let generic_key_layout = get_generic_key_layout_map();
+
+ key_codes
+ .iter()
+ .filter_map(|key_code| generic_key_layout.find_scan_code_for_key(*key_code))
+ .map(|scan_code| int_to_event_code(EventType::EV_KEY as c_uint, scan_code))
+ .collect()
+ }
+}
+
+impl Default for KeyLayoutMapManager {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Get canonical name with all invalid characters replaced by underscores
+fn get_canonical_name(name: &str) -> String {
+ name.chars()
+ .map(|ch| {
+ if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
+ ch
+ } else {
+ '_'
+ }
+ })
+ .collect()
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/mod.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/mod.rs
new file mode 100644
index 0000000000..fa19befdd8
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/mod.rs
@@ -0,0 +1,6 @@
+pub mod generic_key_layout;
+pub mod input_event_lookup;
+pub mod key_layout_file_finder;
+pub mod key_layout_map;
+pub mod key_layout_map_manager;
+pub mod tokenizer;
diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/tokenizer.rs b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/tokenizer.rs
new file mode 100644
index 0000000000..a4b39c0a2b
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/keylayout/tokenizer.rs
@@ -0,0 +1,135 @@
+//! Simple tokenizer for parsing key layout map files.
+//!
+//! This tokenizer tracks position in a buffer and provides methods for
+//! reading tokens, characters, and navigating through lines.
+
+use std::path::PathBuf;
+use std::str;
+
+/// Simple tokenizer for parsing ASCII text files line by line.
+pub struct Tokenizer {
+ file_path: PathBuf,
+ buffer: String,
+ current: usize,
+ line_number: usize,
+}
+
+impl Tokenizer {
+ /// Create a tokenizer from file contents.
+ pub fn from_contents(file_path: PathBuf, contents: &str) -> Self {
+ Self {
+ file_path,
+ buffer: contents.to_string(),
+ current: 0,
+ line_number: 1,
+ }
+ }
+
+ /// Create a tokenizer from a file path.
+ pub fn from_file(file_path: PathBuf) -> Result {
+ let contents = std::fs::read_to_string(file_path.clone())
+ .map_err(|e| format!("Error opening file '{:?}': {}", file_path.clone(), e))?;
+ Ok(Self::from_contents(file_path.clone(), &contents))
+ }
+
+ /// Returns true if at the end of the file.
+ pub fn is_eof(&self) -> bool {
+ self.current >= self.buffer.len()
+ }
+
+ /// Returns true if at the end of the line or end of the file.
+ pub fn is_eol(&self) -> bool {
+ self.is_eof() || self.peek_char() == '\n'
+ }
+
+ /// Formats a location string consisting of the filename and current line number.
+ /// Returns a string like "MyFile.txt:33".
+ pub fn get_location(&self) -> String {
+ format!(
+ "{}:{}",
+ self.file_path.to_str().unwrap_or(""),
+ self.line_number
+ )
+ }
+
+ /// Gets the character at the current position.
+ /// Returns null character at end of file.
+ pub fn peek_char(&self) -> char {
+ if self.is_eof() {
+ '\0'
+ } else {
+ self.buffer[self.current..].chars().next().unwrap_or('\0')
+ }
+ }
+
+ /// Gets the remainder of the current line as a string, excluding the newline character.
+ pub fn peek_remainder_of_line(&self) -> String {
+ if self.is_eof() {
+ return String::new();
+ }
+
+ let remaining = &self.buffer[self.current..];
+ let line_end = remaining
+ .find('\n')
+ .map(|pos| self.current + pos)
+ .unwrap_or(self.buffer.len());
+
+ self.buffer[self.current..line_end].to_string()
+ }
+
+ /// Gets the character at the current position and advances past it.
+ /// Returns null character at end of file.
+ pub fn next_char(&mut self) -> char {
+ if self.is_eof() {
+ return '\0';
+ }
+
+ let ch = self.peek_char();
+ if ch == '\n' {
+ self.line_number += 1;
+ }
+ self.current += ch.len_utf8();
+ ch
+ }
+
+ /// Gets the next token on this line stopping at the specified delimiters
+ /// or the end of the line whichever comes first.
+ /// Returns the token or an empty string if the current character is a delimiter
+ /// or is at the end of the line.
+ pub fn next_token(&mut self, delimiters: &str) -> String {
+ self.skip_delimiters(delimiters);
+
+ if self.is_eol() {
+ return String::new();
+ }
+
+ let start = self.current;
+ while !self.is_eol() && !is_delimiter(self.peek_char(), delimiters) {
+ self.next_char();
+ }
+
+ self.buffer[start..self.current].to_string()
+ }
+
+ /// Advances to the next line.
+ /// Does nothing if already at the end of the file.
+ pub fn next_line(&mut self) {
+ while !self.is_eof() && self.peek_char() != '\n' {
+ self.next_char();
+ }
+ if !self.is_eof() {
+ self.next_char(); // consume the newline
+ }
+ }
+
+ /// Skips over the specified delimiters in the line.
+ pub fn skip_delimiters(&mut self, delimiters: &str) {
+ while !self.is_eol() && is_delimiter(self.peek_char(), delimiters) {
+ self.next_char();
+ }
+ }
+}
+
+fn is_delimiter(ch: char, delimiters: &str) -> bool {
+ delimiters.contains(ch)
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/android/mod.rs b/evdev/src/main/rust/evdev_manager/core/src/android/mod.rs
new file mode 100644
index 0000000000..44bf1d8111
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/android/mod.rs
@@ -0,0 +1,2 @@
+pub mod android_codes;
+pub mod keylayout;
diff --git a/evdev/src/main/rust/evdev_manager/core/src/evdev_device_info.rs b/evdev/src/main/rust/evdev_manager/core/src/evdev_device_info.rs
new file mode 100644
index 0000000000..f11eb31135
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/evdev_device_info.rs
@@ -0,0 +1,8 @@
+#[derive(Hash, Eq, PartialEq, Clone, Debug)]
+pub struct EvdevDeviceInfo {
+ pub name: String,
+ pub bus: u16,
+ pub vendor: u16,
+ pub product: u16,
+ pub version: u16,
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/evdev_devices_watcher.rs b/evdev/src/main/rust/evdev_manager/core/src/evdev_devices_watcher.rs
new file mode 100644
index 0000000000..365a80347a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/evdev_devices_watcher.rs
@@ -0,0 +1,126 @@
+use std::path::{Path, PathBuf};
+use std::sync::{
+ atomic::{AtomicBool, Ordering},
+ mpsc, Arc, Mutex, RwLock,
+};
+
+use notify::{EventKind, RecommendedWatcher, Watcher};
+use tokio::task::JoinHandle;
+
+use crate::evdev_error::{EvdevError, EvdevErrorCode};
+use crate::runtime::get_runtime;
+
+/// Callback for when inotify events occur
+pub trait InotifyCallback: Send + Sync {
+ fn on_inotify_dev_input(&self, paths: &[PathBuf]);
+}
+
+pub struct EvdevDevicesWatcher {
+ watcher: Arc>>,
+ inotify_handle: RwLock
>>,
+ enabled: Arc,
+}
+
+impl EvdevDevicesWatcher {
+ pub fn new() -> Self {
+ Self {
+ watcher: Arc::new(Mutex::new(None)),
+ inotify_handle: RwLock::new(None),
+ enabled: Arc::new(AtomicBool::new(true)),
+ }
+ }
+
+ /// Start the thread to watch /dev/input for device changes
+ pub fn start(&self, callback: Arc) -> Result<(), EvdevError> {
+ let is_running = { self.inotify_handle.read().unwrap().is_some() };
+
+ if is_running {
+ info!("Inotify watcher is already running");
+ return Ok(());
+ }
+
+ // Create the channel and watcher
+ let (tx, rx) = mpsc::channel::>();
+
+ let mut watcher = notify::recommended_watcher(tx).map_err(|e| {
+ error!("Failed to create inotify watcher: {}", e);
+ EvdevError::from_enum(EvdevErrorCode::IoError)
+ })?;
+
+ watcher
+ .watch(Path::new("/dev/input"), notify::RecursiveMode::Recursive)
+ .map_err(|e| {
+ error!("Failed to watch /dev/input: {}", e);
+ EvdevError::from_enum(EvdevErrorCode::IoError)
+ })?;
+
+ // Store the watcher in Arc
+ {
+ let mut watcher_guard = self.watcher.lock().unwrap();
+ *watcher_guard = Some(watcher);
+ }
+
+ // Start the event processing loop
+ let callback_clone = callback.clone();
+ let enabled_clone = self.enabled.clone();
+
+ let handle = get_runtime().spawn(async move {
+ for event_result in rx {
+ // Skip processing if disabled
+ if !enabled_clone.load(Ordering::Relaxed) {
+ continue;
+ }
+
+ match event_result {
+ Ok(event) => {
+ if event.kind == EventKind::Create(notify::event::CreateKind::File)
+ || event.kind == EventKind::Remove(notify::event::RemoveKind::File)
+ {
+ callback_clone.on_inotify_dev_input(&event.paths);
+ }
+ }
+ Err(err) => {
+ error!("Failed to receive inotify event: {}", err);
+ }
+ }
+ }
+ });
+
+ self.inotify_handle.write().unwrap().replace(handle);
+
+ Ok(())
+ }
+
+ /// Stop the thread watching /dev/input for device changes
+ pub fn stop(&self) -> Result<(), EvdevError> {
+ self.enabled.store(false, Ordering::Relaxed);
+
+ let handle_option = self.inotify_handle.write().unwrap().take();
+
+ if let Some(handle) = handle_option {
+ handle.abort();
+ }
+
+ // Clear the watcher
+ let mut watcher_guard = self.watcher.lock().unwrap();
+ *watcher_guard = None;
+
+ Ok(())
+ }
+
+ /// Enable processing of inotify events
+ pub fn enable(&self) {
+ self.enabled.store(true, Ordering::Relaxed);
+ }
+
+ /// Disable processing of inotify events (temporarily skip events)
+ pub fn disable(&self) {
+ self.enabled.store(false, Ordering::Relaxed);
+ }
+}
+
+impl Default for EvdevDevicesWatcher {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/evdev_error.rs b/evdev/src/main/rust/evdev_manager/core/src/evdev_error.rs
new file mode 100644
index 0000000000..49a6028708
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/evdev_error.rs
@@ -0,0 +1,123 @@
+/// Error type for evdev operations
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum EvdevErrorCode {
+ NoSuchFileOrDirectory,
+ IoError,
+ NoSuchDevice,
+ BadFileDescriptor,
+ OutOfMemory,
+ WouldBlock,
+ PermissionDenied,
+ InvalidArgument,
+ Unknown(i32),
+}
+
+impl EvdevErrorCode {
+ pub fn from_code(code: i32) -> Self {
+ match -code {
+ libc::ENOENT => Self::NoSuchFileOrDirectory,
+ libc::EIO => Self::IoError,
+ libc::EBADF => Self::BadFileDescriptor,
+ libc::EAGAIN => Self::WouldBlock,
+ libc::ENOMEM => Self::OutOfMemory,
+ libc::EACCES => Self::PermissionDenied,
+ libc::ENODEV => Self::NoSuchDevice,
+ libc::EINVAL => Self::InvalidArgument,
+ _ => Self::Unknown(code),
+ }
+ }
+
+ pub fn to_code(self) -> i32 {
+ -(match self {
+ Self::NoSuchFileOrDirectory => libc::ENOENT,
+ Self::IoError => libc::EIO,
+ Self::BadFileDescriptor => libc::EBADF,
+ Self::WouldBlock => libc::EAGAIN,
+ Self::OutOfMemory => libc::ENOMEM,
+ Self::PermissionDenied => libc::EACCES,
+ Self::NoSuchDevice => libc::ENODEV,
+ Self::InvalidArgument => libc::EINVAL,
+ Self::Unknown(code) => return code,
+ })
+ }
+
+ pub fn description(&self) -> &'static str {
+ match self {
+ Self::NoSuchFileOrDirectory => "No such file or directory (device not found)",
+ Self::IoError => "Input/output error",
+ Self::NoSuchDevice => "No such device",
+ Self::BadFileDescriptor => "Bad file descriptor",
+ Self::OutOfMemory => "Out of memory",
+ Self::WouldBlock => "Resource temporarily unavailable",
+ Self::PermissionDenied => "Permission denied",
+ Self::InvalidArgument => "Invalid argument",
+ Self::Unknown(_) => "Unknown error",
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct EvdevError {
+ kind: EvdevErrorCode,
+ code: i32,
+ message: String,
+}
+
+impl EvdevError {
+ pub fn new(code: i32) -> Self {
+ let kind = EvdevErrorCode::from_code(code);
+ let message = if let EvdevErrorCode::Unknown(_) = kind {
+ format!("evdev error: {}", code)
+ } else {
+ format!("evdev error: {} ({})", kind.description(), -code)
+ };
+
+ Self {
+ kind,
+ code,
+ message,
+ }
+ }
+ pub fn from_enum(error_code: EvdevErrorCode) -> Self {
+ let message = format!(
+ "evdev error: {} ({})",
+ error_code.description(),
+ -error_code.to_code()
+ );
+
+ Self {
+ kind: error_code,
+ code: error_code.to_code(),
+ message,
+ }
+ }
+
+ pub fn code(&self) -> i32 {
+ self.code
+ }
+
+ pub fn kind(&self) -> EvdevErrorCode {
+ self.kind
+ }
+}
+
+impl std::fmt::Display for EvdevError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.message)
+ }
+}
+
+impl std::error::Error for EvdevError {}
+
+impl From for std::io::Error {
+ fn from(err: EvdevError) -> Self {
+ std::io::Error::from_raw_os_error(-err.code)
+ }
+}
+
+impl From for EvdevError {
+ fn from(err: std::io::Error) -> Self {
+ let code = err.raw_os_error().unwrap_or(-1);
+ EvdevError::new(-code)
+ }
+}
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
new file mode 100644
index 0000000000..848b0d0f9c
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/evdev_grab_controller.rs
@@ -0,0 +1,366 @@
+use std::{
+ error::Error,
+ fs::read_dir,
+ io,
+ os::fd::AsRawFd,
+ path::PathBuf,
+ sync::{Arc, Mutex, RwLock},
+};
+
+use bimap::BiHashMap;
+use evdev::{enums::EventCode, DeviceWrapper};
+use mio::{unix::SourceFd, Interest, Registry, Token};
+use slab::Slab;
+
+use crate::{
+ evdev_device_info::EvdevDeviceInfo,
+ evdev_devices_watcher::{EvdevDevicesWatcher, InotifyCallback},
+ evdev_error::EvdevError,
+ event_loop::EvdevCallback,
+ grab_target::GrabTarget,
+ grabbed_device::GrabbedDevice,
+ grabbed_device_handle::GrabbedDeviceHandle,
+};
+
+pub struct EvdevGrabController {
+ poll_registry: Arc,
+ callback: Arc,
+ grab_targets: Mutex>,
+ grabbed_devices: RwLock>,
+ devices_watcher: EvdevDevicesWatcher,
+}
+
+impl EvdevGrabController {
+ pub fn new(poll_registry: Arc, callback: Arc) -> Self {
+ Self {
+ poll_registry,
+ callback,
+ grab_targets: Mutex::new(Vec::with_capacity(64)),
+ grabbed_devices: RwLock::new(Slab::with_capacity(64)),
+ devices_watcher: EvdevDevicesWatcher::new(),
+ }
+ }
+
+ pub fn set_grab_targets(&self, targets: Vec) -> Vec {
+ info!("Setting grab targets: {:?}", targets);
+
+ let mut grab_targets = self.grab_targets.lock().unwrap();
+
+ grab_targets.clear();
+
+ for target in targets {
+ grab_targets.push(target);
+ }
+
+ let mut grabbed_devices = self.grabbed_devices.write().unwrap();
+ let handles = self.invalidate(grab_targets.as_ref(), &mut grabbed_devices);
+
+ handles
+ }
+
+ fn invalidate(
+ &self,
+ grab_targets: &[GrabTarget],
+ grabbed_devices: &mut Slab,
+ ) -> Vec {
+ // Disable inotify event processing during invalidate to avoid race conditions
+ self.devices_watcher.disable();
+
+ let real_device_paths =
+ Self::get_real_device_paths(grabbed_devices).expect("Unable to evdev device paths");
+ let device_info_path_map = Self::build_device_info_path_map(&real_device_paths);
+
+ let device_keys_to_ungrab = Self::get_devices_to_ungrab(
+ grab_targets,
+ grabbed_devices,
+ device_info_path_map.clone(),
+ );
+
+ // Ungrab devices that are no longer requested
+ for key in device_keys_to_ungrab {
+ let grabbed_device = grabbed_devices.remove(key);
+ self.ungrab_device(grabbed_device);
+ }
+
+ let devices_to_grab =
+ Self::get_targets_to_grab(grab_targets, grabbed_devices, device_info_path_map);
+
+ for (path, extra_event_codes) in devices_to_grab {
+ self.try_grab_target(&path, &extra_event_codes, grabbed_devices)
+ .inspect_err(|err| error!("Failed to grab device {:?}: {:?}", path, err))
+ .ok();
+ }
+
+ let grabbed_device_handles: Vec = grabbed_devices
+ .iter()
+ .map(|(key, device)| GrabbedDeviceHandle::new(key, device.device_info.clone()))
+ .collect();
+
+ info!("Grabbed devices: {:?}", grabbed_device_handles);
+
+ self.callback
+ .on_grabbed_devices_changed(grabbed_device_handles.clone());
+
+ // Re-enable inotify event processing after invalidate is complete
+ self.devices_watcher.enable();
+
+ grabbed_device_handles
+ }
+
+ /// Access a grabbed device by ID through a closure.
+ /// Returns None if the device is not found, otherwise returns the result of the closure.
+ pub fn with_grabbed_device(&self, device_id: usize, f: F) -> Option
+ where
+ F: FnOnce(&GrabbedDevice) -> R,
+ {
+ let grabbed_devices = self.grabbed_devices.read().unwrap();
+ grabbed_devices.get(device_id).map(f)
+ }
+
+ /// Get devices that should be ungrabed based on current grab targets and device state.
+ /// This function is public for testing purposes.
+ pub fn get_devices_to_ungrab(
+ grab_targets: &[GrabTarget],
+ grabbed_devices: &Slab,
+ device_info_path_map: BiHashMap,
+ ) -> Vec {
+ let mut keys_to_remove: Vec = Vec::new();
+
+ for (key, grabbed_device) in grabbed_devices.iter() {
+ let is_connected: bool =
+ device_info_path_map.contains_left(&grabbed_device.device_info);
+
+ if !is_connected {
+ keys_to_remove.push(key);
+ continue;
+ }
+
+ let matching_grab_target: Option<&GrabTarget> = grab_targets
+ .iter()
+ .find(|target| target.matches_device_info(&grabbed_device.device_info));
+
+ match matching_grab_target {
+ // Ungrab if the device should be grabbed with different event codes.
+ Some(target) => {
+ if target.extra_event_codes != grabbed_device.extra_event_codes {
+ keys_to_remove.push(key);
+ continue;
+ }
+ }
+
+ // Ungrab the device if it is no longer targeted to grab
+ None => {
+ keys_to_remove.push(key);
+ continue;
+ }
+ }
+
+ let current_device_at_path: Option<&EvdevDeviceInfo> =
+ device_info_path_map.get_by_right(&grabbed_device.device_path);
+
+ match current_device_at_path {
+ // If the device path cached in the grabbed device no longer points
+ // to the same device
+ Some(real_device_info) => {
+ if grabbed_device.device_info != *real_device_info {
+ keys_to_remove.push(key);
+ continue;
+ }
+ }
+
+ // The path of the grabbed device no longer exists
+ None => {
+ keys_to_remove.push(key);
+ continue;
+ }
+ }
+ }
+
+ keys_to_remove
+ }
+
+ fn ungrab_device(&self, device: GrabbedDevice) {
+ let fd = device.evdev.lock().unwrap().as_raw_fd();
+
+ let mut source_fd = SourceFd(&fd);
+ self.poll_registry
+ .deregister(&mut source_fd)
+ .inspect_err(|e| {
+ error!(
+ "Failed to deregister device {:?}: {:?}",
+ device.device_path, e
+ )
+ })
+ .ok();
+ }
+
+ /// Get targets that should be grabbed based on current grab targets and device state.
+ /// This function is public for testing purposes.
+ pub fn get_targets_to_grab(
+ grab_targets: &[GrabTarget],
+ grabbed_devices: &Slab,
+ device_info_path_map: BiHashMap,
+ ) -> Vec<(PathBuf, Vec)> {
+ let mut targets_to_grab: Vec<(PathBuf, Vec)> = Vec::new();
+
+ for target in grab_targets {
+ let already_grabbed = grabbed_devices
+ .iter()
+ .any(|(_, device)| target.matches_device_info(&device.device_info));
+
+ if already_grabbed {
+ continue;
+ }
+
+ let device_info = device_info_path_map
+ .left_values()
+ .find(|device_info| target.matches_device_info(device_info));
+
+ match device_info {
+ // Target device not connected
+ None => continue,
+ // Device is connected
+ Some(device_info) => {
+ let path = device_info_path_map.get_by_left(device_info).unwrap();
+
+ targets_to_grab.push((path.clone(), target.extra_event_codes.clone()));
+ }
+ }
+ }
+
+ targets_to_grab
+ }
+
+ fn try_grab_target(
+ &self,
+ device_path: &PathBuf,
+ extra_event_codes: &[EventCode],
+ grabbed_devices: &mut Slab,
+ ) -> Result> {
+ let device = GrabbedDevice::new(device_path, extra_event_codes)?;
+ let fd = device.evdev.lock().unwrap().as_raw_fd();
+ let key = grabbed_devices.insert(device);
+
+ let mut source_fd = SourceFd(&fd);
+
+ self.poll_registry
+ .register(&mut source_fd, Token(key), Interest::READABLE)
+ .inspect_err(|e| {
+ // Remove device on registration failure
+ grabbed_devices.remove(key);
+ error!("Failed to register device {:?}: {}", device_path, e);
+ })?;
+
+ Ok(key)
+ }
+
+ pub fn get_real_devices(&self) -> Result, EvdevError> {
+ let grabbed_devices = self.grabbed_devices.read().unwrap();
+
+ let mut list: Vec = Vec::new();
+
+ for path in Self::get_real_device_paths(&grabbed_devices)? {
+ if let Ok(info) = Self::get_device_info(&path) {
+ list.push(info);
+ }
+ }
+
+ Ok(list)
+ }
+
+ /// Get the paths to all the real (non uinput) connected devices.
+ fn get_real_device_paths(
+ grabbed_devices: &Slab,
+ ) -> Result, EvdevError> {
+ let uinput_paths: Vec = grabbed_devices
+ .iter()
+ .map(|(_, device)| device.uinput.devnode().unwrap().into())
+ .collect();
+
+ let mut paths: Vec = Vec::new();
+
+ let dir = read_dir("/dev/input")?;
+
+ for entry_result in dir {
+ match entry_result {
+ Ok(entry) => {
+ let path = entry.path();
+ // Do not return paths to uinput devices that were created.
+ if uinput_paths.contains(&path) {
+ continue;
+ }
+
+ paths.push(path);
+ }
+ Err(_) => {
+ debug!(
+ "Failed to read /dev/input entry: {}",
+ entry_result.unwrap_err()
+ );
+ }
+ }
+ }
+
+ Ok(paths)
+ }
+
+ fn build_device_info_path_map(paths: &[PathBuf]) -> BiHashMap {
+ let mut map: BiHashMap = BiHashMap::new();
+
+ for path in paths {
+ if let Ok(info) = Self::get_device_info(path) {
+ map.insert(info, path.clone());
+ }
+ }
+
+ map
+ }
+
+ fn get_device_info(path: &PathBuf) -> Result {
+ evdev::Device::new_from_path(path).map(|device| EvdevDeviceInfo {
+ name: device.name().unwrap_or("").to_string(),
+ bus: device.bustype(),
+ vendor: device.vendor_id(),
+ product: device.product_id(),
+ version: device.version(),
+ })
+ }
+
+ /// Start watching /dev/input for device changes
+ pub fn start_watching(self: &Arc) -> Result<(), EvdevError> {
+ self.devices_watcher.start(self.clone())
+ }
+
+ /// Stop watching /dev/input for device changes
+ pub fn stop_watching(&self) -> Result<(), EvdevError> {
+ self.devices_watcher.stop()
+ }
+}
+
+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()));
+
+ if is_uinput_device {
+ return;
+ }
+
+ info!("inotify /dev/input event received");
+ let grab_targets = self.grab_targets.lock().unwrap();
+ self.invalidate(grab_targets.as_ref(), &mut grabbed_devices);
+
+ // Notify callback about device list changes
+ drop(grabbed_devices); // Release the write lock before calling get_real_devices
+ match self.get_real_devices() {
+ Ok(devices) => {
+ self.callback.on_evdev_devices_changed(devices);
+ }
+ Err(e) => {
+ error!("Failed to get real devices for callback: {:?}", e);
+ }
+ }
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/event_loop.rs b/evdev/src/main/rust/evdev_manager/core/src/event_loop.rs
new file mode 100644
index 0000000000..ef374aaa5a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/event_loop.rs
@@ -0,0 +1,411 @@
+use crate::android::keylayout::key_layout_map_manager::KeyLayoutMapManager;
+use crate::evdev_device_info::EvdevDeviceInfo;
+use crate::evdev_error::{EvdevError, EvdevErrorCode};
+use crate::evdev_grab_controller::EvdevGrabController;
+use crate::grab_target::GrabTarget;
+use crate::grab_target_key_code::GrabTargetKeyCode;
+use crate::grabbed_device::GrabbedDevice;
+use crate::grabbed_device_handle::GrabbedDeviceHandle;
+use crate::runtime::get_runtime;
+use evdev::enums::{EventType, EV_SYN};
+use evdev::util::event_code_to_int;
+use evdev::{InputEvent, ReadFlag, ReadStatus};
+use libc::c_uint;
+use log::Level;
+use mio::event::Event;
+use mio::{Events, Poll, Token, Waker};
+use std::error::Error;
+use std::io;
+use std::io::ErrorKind;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, OnceLock, RwLock};
+use std::time::{Duration, Instant};
+use std::{fmt, usize};
+use tokio::task::JoinHandle;
+
+/// Callback interface for evdev events and device changes
+pub trait EvdevCallback: Send + Sync {
+ /// Called when an input event is received from a grabbed device.
+ /// Returns true if the callback consumed the event, false to pass through.
+ /// Parameters: device_id (slab key), device_identifier, event
+ fn on_evdev_event(
+ &self,
+ device_id: usize,
+ device_identifier: &EvdevDeviceInfo,
+ event: &InputEvent,
+ ) -> bool;
+
+ /// Called when the list of grabbed devices changes.
+ /// Parameters: grabbed_devices list with their assigned IDs
+ fn on_grabbed_devices_changed(&self, grabbed_devices: Vec);
+
+ /// Called when the list of available evdev devices changes.
+ /// Parameters: devices list of all available evdev devices
+ fn on_evdev_devices_changed(&self, devices: Vec);
+}
+
+static EVENT_LOOP_MANAGER: OnceLock = OnceLock::new();
+
+const WAKER_TOKEN: Token = Token(usize::MAX - 1);
+
+pub struct EventLoopManager {
+ stop_flag: Arc,
+ poll: Arc>,
+ event_loop_handle: RwLock
>>,
+ waker: Waker,
+ callback: Arc,
+ grab_controller: Arc,
+}
+
+impl fmt::Debug for EventLoopManager {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("EventLoopManager")
+ .field("stop_flag", &self.stop_flag.load(Ordering::SeqCst))
+ .field("poll", &"")
+ .field("registry", &"")
+ .field("join_handle", &"")
+ .field("waker", &"")
+ .field("callback", &"")
+ .field("grab_controller", &"")
+ .finish()
+ }
+}
+
+impl EventLoopManager {
+ /// Initialize the EventLoopManager with a callback. Must be called once before `get()`.
+ /// Panics if called more than once.
+ pub fn init(callback: Arc) {
+ EVENT_LOOP_MANAGER
+ .set(Self::new(callback))
+ .expect("EventLoopManager already initialized");
+ }
+
+ /// Get the EventLoopManager instance. Panics if `init()` was not called.
+ pub fn get() -> &'static EventLoopManager {
+ EVENT_LOOP_MANAGER
+ .get()
+ .expect("EventLoopManager not initialized. Call init() first.")
+ }
+
+ fn new(callback: Arc) -> Self {
+ let poll = Poll::new().unwrap();
+ let registry = poll.registry().try_clone().unwrap();
+ let waker = Waker::new(®istry, WAKER_TOKEN).unwrap();
+ let poll_lock = Arc::new(RwLock::new(poll));
+
+ let registry_arc = Arc::new(registry);
+ let grab_controller = EvdevGrabController::new(registry_arc.clone(), callback.clone());
+
+ Self {
+ stop_flag: Arc::new(AtomicBool::new(false)),
+ poll: poll_lock,
+ event_loop_handle: RwLock::new(None),
+ waker,
+ callback,
+ grab_controller: Arc::new(grab_controller),
+ }
+ }
+
+ pub fn start(&self) -> Result<(), EvdevError> {
+ let is_running = { self.event_loop_handle.read().unwrap().is_some() };
+
+ if is_running {
+ info!("EvdevManager event loop is already running");
+ return Ok(());
+ }
+
+ self.stop_flag.store(false, Ordering::Relaxed);
+
+ let callback = self.callback.clone();
+
+ let poll_lock_clone = self.poll.clone();
+ let stop_flag_clone = self.stop_flag.clone();
+ let grab_controller_event_loop = self.grab_controller.clone();
+
+ let event_loop_handle = get_runtime().spawn(async move {
+ EventLoopThread::new(
+ stop_flag_clone,
+ poll_lock_clone,
+ callback,
+ grab_controller_event_loop,
+ )
+ .start();
+ });
+
+ self.event_loop_handle
+ .write()
+ .unwrap()
+ .replace(event_loop_handle);
+
+ self.grab_controller
+ .start_watching()
+ .inspect_err(|err| error!("Failed to start inotify watching: {:?}", err))?;
+
+ Ok(())
+ }
+
+ pub fn stop(&self) -> Result<(), io::Error> {
+ // Stop inotify watching
+ self.grab_controller
+ .stop_watching()
+ .inspect_err(|err| error!("Failed to stop inotify watching: {:?}", err))
+ .ok();
+
+ let handle_option = self.event_loop_handle.write().unwrap().take();
+
+ match handle_option {
+ None => {
+ error!("Event loop not running");
+ }
+ Some(handle) => {
+ self.stop_flag.store(true, Ordering::Relaxed);
+ self.waker.wake()?;
+
+ // Wait for the loop to finish (with timeout)
+ let start = Instant::now();
+ while !handle.is_finished() {
+ if start.elapsed() > Duration::from_secs(2) {
+ error!("Event loop did not stop in time, aborting");
+ handle.abort();
+
+ return Err(io::Error::new(
+ ErrorKind::TimedOut,
+ "Event loop did not stop in time",
+ ));
+ }
+ std::thread::sleep(Duration::from_millis(10));
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Set the list of grabbed devices. This will ungrab any devices that are no longer in the list
+ /// and grab any new devices. Devices are matched by DeviceIdentifier (name, bus, vendor, product).
+ /// Returns: A list of (device_id, DeviceIdentifier) for all successfully grabbed devices.
+ pub fn set_grab_targets(&self, targets: Vec) -> Vec {
+ let internal_grab_targets = targets.iter().map(Self::convert_grab_target).collect();
+
+ let handles = self.grab_controller.set_grab_targets(internal_grab_targets);
+
+ for handle in handles.clone() {
+ KeyLayoutMapManager::get()
+ .preload_key_layout_map(&handle.device_info)
+ .inspect_err(|err| {
+ error!(
+ "Failed to preload key layout map for device {:?}: {}",
+ handle.device_info, err
+ );
+ })
+ .ok();
+ }
+
+ handles
+ }
+
+ pub fn get_real_devices(&self) -> Result, EvdevError> {
+ self.grab_controller.get_real_devices()
+ }
+
+ /// Write an event to a grabbed device's uinput.
+ /// The device_id is the slab key returned by set_grabbed_devices(), enabling O(1) lookup.
+ pub fn write_event(
+ &self,
+ device_id: usize,
+ event_type: u32,
+ code: u32,
+ value: i32,
+ ) -> Result<(), EvdevError> {
+ if log_enabled!(Level::Debug) {
+ debug!(
+ "Write evdev event: device_id={} event_type={} code={} value={}",
+ device_id, event_type, code, value
+ );
+ }
+
+ self.grab_controller
+ .with_grabbed_device(device_id, |device| {
+ device
+ .uinput
+ .write_event(event_type, code, value)
+ .map_err(EvdevError::from)
+ })
+ .ok_or(EvdevError::from_enum(EvdevErrorCode::NoSuchDevice))?
+ }
+
+ pub fn write_key_code_event(
+ &self,
+ device_id: usize,
+ key_code: u32,
+ value: i32,
+ ) -> Result<(), Box> {
+ let result = self
+ .grab_controller
+ .with_grabbed_device(device_id, |device| {
+ let scan_code_result = KeyLayoutMapManager::get()
+ .find_scan_code_for_key(&device.device_info, key_code);
+
+ match scan_code_result {
+ Err(e) => Err(e.into()),
+ Ok(None) => {
+ error!("Failed to find scan code for key: {}", key_code);
+ Err(Box::new(EvdevError::new(-libc::ENODATA)) as Box)
+ }
+ Ok(Some(code)) => {
+ if log_enabled!(Level::Debug) {
+ debug!(
+ "Write key code evdev event: key_code={} value={}",
+ key_code, value
+ );
+ }
+
+ device
+ .uinput
+ .write_event(EventType::EV_KEY as c_uint, code, value)
+ .map_err(|err| Box::new(err) as Box)?;
+
+ // Send SYN_REPORT
+ device
+ .uinput
+ .write_event(
+ EventType::EV_SYN as c_uint,
+ EV_SYN::SYN_REPORT as c_uint,
+ 0,
+ )
+ .map_err(|err| Box::new(err) as Box)
+ }
+ }
+ });
+
+ match result {
+ Some(inner_result) => inner_result,
+ None => Err(Box::new(EvdevError::from_enum(
+ EvdevErrorCode::NoSuchDevice,
+ ))),
+ }
+ }
+
+ fn convert_grab_target(target: &GrabTargetKeyCode) -> GrabTarget {
+ let event_codes =
+ KeyLayoutMapManager::map_key_codes_to_event_codes(&target.extra_key_codes);
+
+ GrabTarget {
+ name: target.name.clone(),
+ bus: target.bus,
+ vendor: target.vendor,
+ product: target.product,
+ extra_event_codes: event_codes,
+ }
+ }
+}
+
+struct EventLoopThread {
+ stop_flag: Arc,
+ poll: Arc>,
+ callback: Arc,
+ grab_controller: Arc,
+}
+
+impl EventLoopThread {
+ pub fn new(
+ stop_flag: Arc,
+ poll: Arc>,
+ callback: Arc,
+ grab_controller: Arc,
+ ) -> Self {
+ EventLoopThread {
+ stop_flag,
+ poll,
+ callback,
+ grab_controller,
+ }
+ }
+
+ /// This blocks until the stop flag is set.
+ fn start(&self) {
+ let mut events = Events::with_capacity(128);
+
+ info!("Started evdev event loop");
+
+ 'main: loop {
+ let mut poll = self.poll.write().unwrap();
+
+ match poll.poll(&mut events, None) {
+ Ok(_) => {
+ for event in events.iter() {
+ // Break out of the loop if the stop flag is set.
+ if event.token() == WAKER_TOKEN && self.stop_flag.load(Ordering::SeqCst) {
+ info!("Received waker token. Stopping evdev event loop");
+ break 'main;
+ }
+
+ self.on_poll_event(event);
+ }
+ }
+ Err(e) if e.kind() == ErrorKind::Interrupted => {
+ // Interrupted, continue polling
+ continue;
+ }
+ Err(e) => {
+ error!("EvdevManager poll error. Stopping loop: {}", e);
+ break;
+ }
+ }
+ }
+
+ info!("Stopped evdev event loop");
+ }
+
+ fn on_poll_event(&self, event: &Event) {
+ let Token(key) = event.token();
+ let slab_key = key;
+
+ self.grab_controller
+ .with_grabbed_device(slab_key, |device| {
+ let evdev = device.evdev.lock().unwrap();
+ let mut flags: ReadFlag = ReadFlag::NORMAL;
+
+ loop {
+ match evdev.next_event(flags) {
+ Ok((ReadStatus::Success, input_event)) => {
+ flags = ReadFlag::NORMAL;
+ if log_enabled!(Level::Debug) {
+ debug!("Evdev event: {:?}", input_event);
+ }
+ self.process_event(slab_key, &input_event, device);
+ }
+ Ok((ReadStatus::Sync, _event)) => {
+ // Continue reading sync events
+ flags = ReadFlag::NORMAL | ReadFlag::SYNC;
+ }
+ Err(_error) => {
+ // Break if it's EAGAIN (no more events) or any other error.
+ // Do not log these errors because it is expected
+ break;
+ }
+ }
+ }
+ });
+ }
+
+ fn process_event(&self, device_id: usize, event: &InputEvent, grabbed_device: &GrabbedDevice) {
+ let consumed = self
+ .callback
+ .on_evdev_event(device_id, &grabbed_device.device_info, event);
+
+ if !consumed {
+ let (event_type, event_code) = event_code_to_int(&event.event_code);
+ grabbed_device
+ .uinput
+ .write_event(event_type, event_code, event.value)
+ .inspect_err(|e| {
+ error!(
+ "Failed to passthrough event to {:?}. Event: {:?}. Error: {:?}",
+ grabbed_device.device_path, event, e
+ )
+ })
+ .ok();
+ }
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/grab_target.rs b/evdev/src/main/rust/evdev_manager/core/src/grab_target.rs
new file mode 100644
index 0000000000..248569b18a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/grab_target.rs
@@ -0,0 +1,25 @@
+use evdev::enums::EventCode;
+
+use crate::evdev_device_info::EvdevDeviceInfo;
+
+/// The information required to grab a device.
+///
+#[derive(Hash, Eq, PartialEq, Clone, Debug)]
+pub struct GrabTarget {
+ pub name: String,
+ pub bus: u16,
+ pub vendor: u16,
+ pub product: u16,
+ /// The extra event codes that should be enabled for the device. This is so that the
+ /// uinput device can input events that the original device didn't support.
+ pub extra_event_codes: Vec,
+}
+
+impl GrabTarget {
+ pub fn matches_device_info(&self, device_info: &EvdevDeviceInfo) -> bool {
+ device_info.name == self.name
+ && device_info.bus == self.bus
+ && device_info.vendor == self.vendor
+ && device_info.product == self.product
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/grab_target_key_code.rs b/evdev/src/main/rust/evdev_manager/core/src/grab_target_key_code.rs
new file mode 100644
index 0000000000..26d81c1358
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/grab_target_key_code.rs
@@ -0,0 +1,8 @@
+#[derive(Debug)]
+pub struct GrabTargetKeyCode {
+ pub name: String,
+ pub bus: u16,
+ pub vendor: u16,
+ pub product: u16,
+ pub extra_key_codes: Vec,
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/grabbed_device.rs b/evdev/src/main/rust/evdev_manager/core/src/grabbed_device.rs
new file mode 100644
index 0000000000..861d995705
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/grabbed_device.rs
@@ -0,0 +1,75 @@
+use crate::evdev_device_info::EvdevDeviceInfo;
+use crate::evdev_error::EvdevError;
+use evdev::enums::EventCode;
+use evdev::{Device, DeviceWrapper, GrabMode, UInputDevice};
+use std::fs::OpenOptions;
+use std::os::unix::fs::OpenOptionsExt;
+use std::path::PathBuf;
+use std::sync::Mutex;
+
+/// Device context containing all information about a grabbed evdev device
+pub struct GrabbedDevice {
+ pub device_path: PathBuf,
+ pub device_info: EvdevDeviceInfo,
+ /// The libevdev Device can not be shared safely across threads so wrap it in a mutex.
+ pub evdev: Mutex,
+ pub uinput: UInputDevice,
+ /// The extra event codes that were enabled for the uinput device. This is so that the
+ /// uinput device can input events that the original device didn't support.
+ pub extra_event_codes: Vec,
+}
+
+impl GrabbedDevice {
+ /// Create a grabbed device that also enables the given EventCodes in the uinput device.
+ pub fn new(device_path: &PathBuf, extra_events: &[EventCode]) -> Result {
+ let mut evdev = Self::open_evdev_device(device_path)?;
+
+ for event in extra_events {
+ evdev.enable(*event)?;
+ }
+
+ evdev.grab(GrabMode::Grab).map_err(EvdevError::from)?;
+ let uinput = UInputDevice::create_from_device(&evdev).map_err(EvdevError::from)?;
+
+ let device_info = EvdevDeviceInfo {
+ name: evdev.name().unwrap_or("").to_string(),
+ bus: evdev.bustype(),
+ vendor: evdev.vendor_id(),
+ product: evdev.product_id(),
+ version: evdev.version(),
+ };
+
+ Ok(Self {
+ device_path: device_path.clone(),
+ device_info,
+ evdev: Mutex::new(evdev),
+ uinput,
+ extra_event_codes: extra_events.into(),
+ })
+ }
+
+ fn open_evdev_device(device_path: &PathBuf) -> Result {
+ // Open device with O_NONBLOCK so that the loop reading events eventually returns
+ // due to an EAGAIN error
+
+ let file = OpenOptions::new()
+ .read(true)
+ .custom_flags(libc::O_NONBLOCK)
+ .open(device_path)
+ .map_err(EvdevError::from)?;
+
+ let evdev = Device::new_from_file(file).map_err(EvdevError::from)?;
+ Ok(evdev)
+ }
+}
+
+impl Drop for GrabbedDevice {
+ fn drop(&mut self) {
+ let mut evdev = self.evdev.lock().unwrap();
+ // Ungrab the device
+ evdev
+ .grab(GrabMode::Ungrab)
+ .inspect_err(|err| error!("Failed to ungrab device {:?}; {}", self.device_info, err))
+ .ok();
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/grabbed_device_handle.rs b/evdev/src/main/rust/evdev_manager/core/src/grabbed_device_handle.rs
new file mode 100644
index 0000000000..5c1d334250
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/grabbed_device_handle.rs
@@ -0,0 +1,14 @@
+use crate::evdev_device_info::EvdevDeviceInfo;
+
+/// Handle to a grabbed device with its assigned ID for O(1) lookup
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct GrabbedDeviceHandle {
+ pub id: usize,
+ pub device_info: EvdevDeviceInfo,
+}
+
+impl GrabbedDeviceHandle {
+ pub fn new(id: usize, device_info: EvdevDeviceInfo) -> Self {
+ Self { id, device_info }
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/src/lib.rs b/evdev/src/main/rust/evdev_manager/core/src/lib.rs
new file mode 100644
index 0000000000..40b0c8b813
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/lib.rs
@@ -0,0 +1,13 @@
+#[macro_use]
+extern crate log;
+pub mod android;
+pub mod evdev_device_info;
+pub mod evdev_devices_watcher;
+pub mod evdev_error;
+pub mod evdev_grab_controller;
+pub mod event_loop;
+pub mod grab_target;
+pub mod grab_target_key_code;
+pub mod grabbed_device;
+pub mod grabbed_device_handle;
+pub mod runtime;
diff --git a/evdev/src/main/rust/evdev_manager/core/src/runtime.rs b/evdev/src/main/rust/evdev_manager/core/src/runtime.rs
new file mode 100644
index 0000000000..ba238adcf6
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/src/runtime.rs
@@ -0,0 +1,14 @@
+use std::sync::OnceLock;
+use tokio::runtime::Runtime;
+
+static RUNTIME: OnceLock = OnceLock::new();
+
+pub fn get_runtime() -> &'static Runtime {
+ RUNTIME.get_or_init(|| {
+ tokio::runtime::Builder::new_multi_thread()
+ .worker_threads(2) // Optional: limit threads to save resources on Android
+ .enable_all()
+ .build()
+ .expect("Failed to create Tokio runtime")
+ })
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/input_event_lookup_test.rs b/evdev/src/main/rust/evdev_manager/core/tests/input_event_lookup_test.rs
new file mode 100644
index 0000000000..0d4189f0f3
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/input_event_lookup_test.rs
@@ -0,0 +1,32 @@
+//! Integration tests for key layout map parsing.
+
+use evdev_manager_core::android::android_codes::{
+ AKEYCODE_A, AKEYCODE_FUNCTION, AMOTION_EVENT_AXIS_X, POLICY_FLAG_VIRTUAL,
+};
+use evdev_manager_core::android::keylayout::input_event_lookup;
+#[cfg(test)]
+use pretty_assertions::assert_eq;
+
+#[test]
+fn test_function_key_lookup() {
+ let key_code = input_event_lookup::get_key_code_by_label("FUNCTION");
+ assert_eq!(key_code, Some(AKEYCODE_FUNCTION));
+}
+
+#[test]
+fn test_letter_key_lookup() {
+ let key_code = input_event_lookup::get_key_code_by_label("A");
+ assert_eq!(key_code, Some(AKEYCODE_A));
+}
+
+#[test]
+fn test_policy_flag_lookup() {
+ let code = input_event_lookup::get_key_flag_by_label("VIRTUAL");
+ assert_eq!(code, Some(POLICY_FLAG_VIRTUAL));
+}
+
+#[test]
+fn test_axis_lookup() {
+ let code = input_event_lookup::get_axis_by_label("X");
+ assert_eq!(code, Some(AMOTION_EVENT_AXIS_X));
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/key_layout_map_manager_test.rs b/evdev/src/main/rust/evdev_manager/core/tests/key_layout_map_manager_test.rs
new file mode 100644
index 0000000000..5f80f13d0a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/key_layout_map_manager_test.rs
@@ -0,0 +1,698 @@
+//! Tests for KeyLayoutMapManager file finding logic.
+use assertables::{assert_iter_eq, assert_some, assert_some_eq};
+use evdev_manager_core::android::android_codes::{
+ AKEYCODE_ESCAPE, AKEYCODE_HOME, AKEYCODE_MINUS, AKEYCODE_MOVE_HOME, AKEYCODE_SPACE,
+};
+use evdev_manager_core::android::keylayout::key_layout_file_finder::KeyLayoutFileFinder;
+use evdev_manager_core::android::keylayout::key_layout_map_manager::{
+ get_generic_key_layout_map, KeyLayoutMapManager,
+};
+use evdev_manager_core::evdev_device_info::EvdevDeviceInfo;
+#[cfg(test)]
+use pretty_assertions::assert_eq;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+/// Mock file finder that returns predefined paths for testing.
+struct MockFileFinder {
+ system_files: HashMap,
+ key_mapper_files: HashMap,
+}
+
+impl MockFileFinder {
+ fn new() -> Self {
+ Self {
+ system_files: HashMap::new(),
+ key_mapper_files: HashMap::new(),
+ }
+ }
+
+ fn add_system_file(mut self, name: &str, path: PathBuf) -> Self {
+ self.system_files.insert(name.to_string(), path);
+ self
+ }
+}
+
+impl KeyLayoutFileFinder for MockFileFinder {
+ fn find_system_key_layout_file_by_name(&self, name: &str) -> Option {
+ self.system_files.get(name).cloned()
+ }
+
+ fn find_key_mapper_key_layout_file_by_name(&self, name: &str) -> Option {
+ self.key_mapper_files.get(name).cloned()
+ }
+}
+
+fn find_key_layout_files_str(
+ mock_file_finder: Arc,
+ device: &EvdevDeviceInfo,
+) -> Vec {
+ let manager = KeyLayoutMapManager::with_file_finder(mock_file_finder);
+
+ manager
+ .find_key_layout_files(&device)
+ .iter()
+ .map(|path| path.to_str().unwrap().to_string())
+ .collect()
+}
+
+fn get_test_data_path() -> PathBuf {
+ let test_data_dir = env!("CARGO_MANIFEST_DIR");
+ PathBuf::from(test_data_dir).join("tests").join("test_data")
+}
+
+#[test]
+fn test_find_no_files() {
+ let mock_finder = Arc::new(MockFileFinder::new());
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x1234,
+ product: 0x5678,
+ version: 0x0001,
+ };
+
+ let files: Vec = find_key_layout_files_str(mock_finder, &device);
+
+ assert_eq!(files.len(), 0)
+}
+
+#[test]
+fn test_only_find_generic_file() {
+ let mock_finder = Arc::new(
+ MockFileFinder::new()
+ .add_system_file("Generic", PathBuf::from("/system/usr/keylayout/Generic.kl")),
+ );
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x1234,
+ product: 0x5678,
+ version: 0x0001,
+ };
+
+ let files: Vec = find_key_layout_files_str(mock_finder, &device);
+
+ let expected = ["/system/usr/keylayout/Generic.kl"];
+
+ assert_iter_eq!(files, expected);
+}
+
+#[test]
+fn test_find_key_layout_files_priority_order() {
+ let mock_finder = Arc::new(
+ MockFileFinder::new()
+ .add_system_file(
+ "Vendor_1234_Product_5678_Version_0001",
+ PathBuf::from("/system/usr/keylayout/Vendor_1234_Product_5678_Version_0001.kl"),
+ )
+ .add_system_file(
+ "Vendor_1234_Product_5678",
+ PathBuf::from("/system/usr/keylayout/Vendor_1234_Product_5678.kl"),
+ )
+ .add_system_file(
+ "gpio-keys",
+ PathBuf::from("/system/usr/keylayout/gpio-keys.kl"),
+ )
+ .add_system_file("Generic", PathBuf::from("/system/usr/keylayout/Generic.kl")),
+ );
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x1234,
+ product: 0x5678,
+ version: 0x0001,
+ };
+
+ let files: Vec = find_key_layout_files_str(mock_finder, &device);
+
+ let expected = [
+ "/system/usr/keylayout/Vendor_1234_Product_5678_Version_0001.kl",
+ "/system/usr/keylayout/Vendor_1234_Product_5678.kl",
+ "/system/usr/keylayout/gpio-keys.kl",
+ "/system/usr/keylayout/Generic.kl",
+ ];
+
+ assert_iter_eq!(files, expected);
+}
+
+#[test]
+fn test_preload_key_layout_map_returns_cached_result() {
+ // Create a mock file finder that returns a path to a valid key layout file
+ let test_kl_path = get_test_data_path().join("Generic.kl");
+ let mock_finder =
+ Arc::new(MockFileFinder::new().add_system_file("Generic", test_kl_path.clone()));
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Test Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x0000,
+ product: 0x0000,
+ version: 0x0000,
+ };
+
+ let result1 = manager.preload_key_layout_map(&device).unwrap();
+ // Verify the cached value is returned by comparing the pointers
+ let result2 = manager.preload_key_layout_map(&device).unwrap();
+
+ assert!(
+ Arc::ptr_eq(&result1.unwrap(), &result2.unwrap()),
+ "Second call should return the same cached Arc instance"
+ );
+}
+
+#[test]
+fn test_preload_key_layout_map_saves_some_when_found() {
+ // Create a mock file finder that returns no files
+ let test_kl_path = get_test_data_path().join("Generic.kl");
+ let mock_finder =
+ Arc::new(MockFileFinder::new().add_system_file("Generic", test_kl_path.clone()));
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Test Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ assert_some!(manager.preload_key_layout_map(&device).unwrap());
+ let cache = manager.key_layout_maps.lock().unwrap();
+ assert!(cache.contains_key(&device));
+ assert_some!(cache.get(&device).unwrap());
+}
+
+#[test]
+fn test_preload_key_layout_map_uses_fallback_when_not_found() {
+ // Create a mock file finder that returns no files
+ let mock_finder = Arc::new(MockFileFinder::new());
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Unknown Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Should return the hardcoded fallback, not None
+ assert_some!(manager.preload_key_layout_map(&device).unwrap());
+ let cache = manager.key_layout_maps.lock().unwrap();
+ assert!(cache.contains_key(&device));
+ assert_some!(cache.get(&device).unwrap());
+}
+
+#[test]
+fn test_map_key_reads_from_cache() {
+ // Create a mock file finder that returns no files
+ let test_kl_path = get_test_data_path().join("Generic.kl");
+ let mock_finder =
+ Arc::new(MockFileFinder::new().add_system_file("Generic", test_kl_path.clone()));
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Test Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ let cached_key_layout_map = manager.preload_key_layout_map(&device).unwrap().unwrap();
+ let cached_key = cached_key_layout_map.map_key(12).unwrap();
+
+ // Verify the cached value is returned
+ let map_key_result = manager.map_key(&device, 12).unwrap().unwrap();
+
+ assert_eq!(cached_key, map_key_result, "Key codes should be equal");
+}
+
+#[test]
+fn test_map_key_saves_to_cache() {
+ // Create a mock file finder that returns no files
+ let test_kl_path = get_test_data_path().join("Generic.kl");
+ let mock_finder =
+ Arc::new(MockFileFinder::new().add_system_file("Generic", test_kl_path.clone()));
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Test Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Verify the cached value is returned
+ let cached_key = manager.map_key(&device, 12).unwrap().unwrap();
+ let map_key = manager.map_key(&device, 12).unwrap().unwrap();
+
+ assert_eq!(cached_key, map_key, "Key codes should be equal");
+}
+
+#[test]
+fn test_map_key_finds_file_if_cache_miss() {
+ // Create a mock file finder that returns no files
+ let test_kl_path = get_test_data_path().join("Generic.kl");
+ let mock_finder =
+ Arc::new(MockFileFinder::new().add_system_file("Generic", test_kl_path.clone()));
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Test Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Verify the key is found and has the correct key code
+ let map_key_result = manager.map_key(&device, 12).unwrap().unwrap();
+
+ assert_eq!(map_key_result, AKEYCODE_MINUS);
+}
+
+#[test]
+fn test_map_key_reads_first_found_path() {
+ // Create a mock file finder that returns no files
+ let mock_finder = MockFileFinder::new()
+ .add_system_file("Generic", get_test_data_path().join("Generic.kl"))
+ .add_system_file("gpio-keys", get_test_data_path().join("6t/gpio-keys.kl"));
+
+ let manager = KeyLayoutMapManager::with_file_finder(Arc::new(mock_finder));
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Verify the correct key code is returned (device-specific file takes priority)
+ let map_key_result = manager.map_key(&device, 102).unwrap().unwrap();
+
+ // In gpio-keys.kl this is HOME and in Generic.kl this is MOVE_HOME
+ assert_eq!(map_key_result, AKEYCODE_HOME);
+}
+
+#[test]
+fn test_map_key_reads_generic_if_device_specific_not_found() {
+ // Create a mock file finder that returns no files
+ let mock_finder =
+ MockFileFinder::new().add_system_file("Generic", get_test_data_path().join("Generic.kl"));
+
+ let manager = KeyLayoutMapManager::with_file_finder(Arc::new(mock_finder));
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Verify the fallback to Generic.kl works
+ let map_key_result = manager.map_key(&device, 102).unwrap().unwrap();
+
+ // In gpio-keys.kl this is HOME and in Generic.kl this is MOVE_HOME
+ assert_eq!(map_key_result, AKEYCODE_MOVE_HOME);
+}
+
+#[test]
+fn test_get_generic_key_layout_map_returns_valid_map() {
+ let generic_map = get_generic_key_layout_map();
+
+ // Test some well-known key mappings from Generic.kl
+ assert_eq!(generic_map.map_key(1), Some(AKEYCODE_ESCAPE)); // ESCAPE
+ assert_eq!(generic_map.map_key(57), Some(AKEYCODE_SPACE)); // SPACE
+ assert_eq!(generic_map.map_key(102), Some(AKEYCODE_MOVE_HOME)); // MOVE_HOME
+}
+
+#[test]
+fn test_get_generic_key_layout_map_is_static() {
+ // Calling get_generic_key_layout_map multiple times should return the same instance
+ let map1 = get_generic_key_layout_map();
+ let map2 = get_generic_key_layout_map();
+
+ // They should be the same Arc (same pointer)
+ assert!(Arc::ptr_eq(&map1, &map2));
+}
+
+#[test]
+fn test_get_generic_key_layout_map_reverse_lookup() {
+ let generic_map = get_generic_key_layout_map();
+
+ let escape_scan_code = generic_map.find_scan_code_for_key(AKEYCODE_ESCAPE);
+ assert_some_eq!(escape_scan_code, Some(1));
+}
+
+#[test]
+fn test_fallback_to_hardcoded_generic_when_no_files_found() {
+ // Create a mock file finder that returns no files at all
+ let mock_finder = Arc::new(MockFileFinder::new());
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Unknown Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Should still return a valid key layout map (the hardcoded fallback)
+ let result = manager.preload_key_layout_map(&device).unwrap();
+ assert!(result.is_some(), "Should return hardcoded Generic fallback");
+
+ // Verify the fallback map has the expected mappings
+ let map = result.unwrap();
+ assert_eq!(map.map_key(1), Some(AKEYCODE_ESCAPE)); // ESCAPE
+ assert_eq!(map.map_key(57), Some(AKEYCODE_SPACE)); // SPACE
+ assert_eq!(map.map_key(102), Some(AKEYCODE_MOVE_HOME)); // MOVE_HOME
+}
+
+#[test]
+fn test_fallback_map_key_works_when_no_files_found() {
+ // Create a mock file finder that returns no files
+ let mock_finder = Arc::new(MockFileFinder::new());
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Unknown Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // map_key should work using the hardcoded fallback
+ let key_code = manager.map_key(&device, 1).unwrap();
+ assert_eq!(key_code, Some(AKEYCODE_ESCAPE));
+
+ let key_code = manager.map_key(&device, 57).unwrap();
+ assert_eq!(key_code, Some(AKEYCODE_SPACE));
+
+ // Unknown scan code should return None
+ let key_code = manager.map_key(&device, 9999).unwrap();
+ assert_eq!(key_code, None);
+}
+
+#[test]
+fn test_fallback_is_cached() {
+ // Create a mock file finder that returns no files
+ let mock_finder = Arc::new(MockFileFinder::new());
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Unknown Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // First call should use the fallback
+ let result1 = manager.preload_key_layout_map(&device).unwrap().unwrap();
+
+ // Second call should return the same cached instance
+ let result2 = manager.preload_key_layout_map(&device).unwrap().unwrap();
+
+ assert!(
+ Arc::ptr_eq(&result1, &result2),
+ "Fallback should be cached and return same Arc instance"
+ );
+}
+
+#[test]
+fn test_fallback_uses_static_generic_map() {
+ // Create a mock file finder that returns no files
+ let mock_finder = Arc::new(MockFileFinder::new());
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Unknown Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Get the fallback via manager
+ let manager_map = manager.preload_key_layout_map(&device).unwrap().unwrap();
+
+ // Get the static generic map directly
+ let static_map = get_generic_key_layout_map();
+
+ // They should be the same Arc instance
+ assert!(
+ Arc::ptr_eq(&manager_map, &static_map),
+ "Manager fallback should use the static generic map"
+ );
+}
+
+#[test]
+fn test_map_key_falls_back_to_generic_for_missing_scan_code() {
+ // gpio-keys.kl only has a few keys (like HOME on scan code 102)
+ // It doesn't have ESCAPE (scan code 1), so it should fall back to Generic
+ let mock_finder = Arc::new(
+ MockFileFinder::new()
+ .add_system_file("gpio-keys", get_test_data_path().join("6t/gpio-keys.kl")),
+ );
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x0000,
+ product: 0x0000,
+ version: 0x0000,
+ };
+
+ // Scan code 102 is in gpio-keys.kl and maps to HOME
+ let key_code = manager.map_key(&device, 102).unwrap();
+ assert_eq!(key_code, Some(AKEYCODE_HOME));
+
+ // Scan code 1 (ESCAPE) is NOT in gpio-keys.kl, so it should fall back to Generic
+ let key_code = manager.map_key(&device, 1).unwrap();
+ assert_eq!(key_code, Some(AKEYCODE_ESCAPE));
+
+ // Scan code 57 (SPACE) is NOT in gpio-keys.kl, so it should fall back to Generic
+ let key_code = manager.map_key(&device, 57).unwrap();
+ assert_eq!(key_code, Some(AKEYCODE_SPACE));
+}
+
+#[test]
+fn test_find_scan_code_for_key_falls_back_to_generic_for_missing_key_code() {
+ // gpio-keys.kl only has a few keys
+ // It doesn't have ESCAPE, so reverse lookup should fall back to Generic
+ let mock_finder = Arc::new(
+ MockFileFinder::new()
+ .add_system_file("gpio-keys", get_test_data_path().join("6t/gpio-keys.kl")),
+ );
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x0000,
+ product: 0x0000,
+ version: 0x0000,
+ };
+
+ // HOME is in gpio-keys.kl, should return scan code 102
+ let scan_code = manager
+ .find_scan_code_for_key(&device, AKEYCODE_HOME)
+ .unwrap();
+ assert_eq!(scan_code, Some(102));
+
+ // ESCAPE is NOT in gpio-keys.kl, should fall back to Generic (scan code 1)
+ let scan_code = manager
+ .find_scan_code_for_key(&device, AKEYCODE_ESCAPE)
+ .unwrap();
+ assert_eq!(scan_code, Some(1));
+
+ // SPACE is NOT in gpio-keys.kl, should fall back to Generic (scan code 57)
+ let scan_code = manager
+ .find_scan_code_for_key(&device, AKEYCODE_SPACE)
+ .unwrap();
+ assert_eq!(scan_code, Some(57));
+}
+
+#[test]
+fn test_find_scan_code_for_key_reads_from_cache() {
+ let test_kl_path = get_test_data_path().join("Generic.kl");
+ let mock_finder =
+ Arc::new(MockFileFinder::new().add_system_file("Generic", test_kl_path.clone()));
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Test Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ let cached_key_layout_map = manager.preload_key_layout_map(&device).unwrap().unwrap();
+ let cached_scan_code = cached_key_layout_map
+ .find_scan_code_for_key(AKEYCODE_MINUS)
+ .unwrap();
+
+ // Verify the cached value is returned
+ let scan_code_result = manager
+ .find_scan_code_for_key(&device, AKEYCODE_MINUS)
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(
+ cached_scan_code, scan_code_result,
+ "Scan codes should be equal"
+ );
+}
+
+#[test]
+fn test_find_scan_code_for_key_saves_to_cache() {
+ let test_kl_path = get_test_data_path().join("Generic.kl");
+ let mock_finder =
+ Arc::new(MockFileFinder::new().add_system_file("Generic", test_kl_path.clone()));
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Test Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // First call loads and caches the map
+ let first_result = manager
+ .find_scan_code_for_key(&device, AKEYCODE_MINUS)
+ .unwrap()
+ .unwrap();
+ // Second call should use the cache
+ let second_result = manager
+ .find_scan_code_for_key(&device, AKEYCODE_MINUS)
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(first_result, second_result, "Scan codes should be equal");
+}
+
+#[test]
+fn test_find_scan_code_for_key_finds_file_if_cache_miss() {
+ let test_kl_path = get_test_data_path().join("Generic.kl");
+ let mock_finder =
+ Arc::new(MockFileFinder::new().add_system_file("Generic", test_kl_path.clone()));
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Test Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // MINUS key code should map to scan code 12 in Generic.kl
+ let scan_code_result = manager
+ .find_scan_code_for_key(&device, AKEYCODE_MINUS)
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(scan_code_result, 12);
+}
+
+#[test]
+fn test_find_scan_code_for_key_reads_first_found_path() {
+ // gpio-keys.kl has HOME -> 102, Generic.kl has HOME -> 172
+ let mock_finder = MockFileFinder::new()
+ .add_system_file("Generic", get_test_data_path().join("Generic.kl"))
+ .add_system_file("gpio-keys", get_test_data_path().join("6t/gpio-keys.kl"));
+
+ let manager = KeyLayoutMapManager::with_file_finder(Arc::new(mock_finder));
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Device-specific file (gpio-keys.kl) should take priority
+ // In gpio-keys.kl, HOME maps to scan code 102
+ let scan_code_result = manager
+ .find_scan_code_for_key(&device, AKEYCODE_HOME)
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(scan_code_result, 102);
+}
+
+#[test]
+fn test_find_scan_code_for_key_reads_generic_if_device_not_found() {
+ let mock_finder =
+ MockFileFinder::new().add_system_file("Generic", get_test_data_path().join("Generic.kl"));
+
+ let manager = KeyLayoutMapManager::with_file_finder(Arc::new(mock_finder));
+
+ let device = EvdevDeviceInfo {
+ name: "gpio-keys".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // No device-specific file found, fallback to Generic.kl
+ // In Generic.kl, MOVE_HOME maps to scan code 102
+ let scan_code_result = manager
+ .find_scan_code_for_key(&device, AKEYCODE_MOVE_HOME)
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(scan_code_result, 102);
+}
+
+#[test]
+fn test_find_scan_code_for_key_returns_none_for_unknown_key() {
+ let mock_finder = Arc::new(MockFileFinder::new());
+
+ let manager = KeyLayoutMapManager::with_file_finder(mock_finder);
+
+ let device = EvdevDeviceInfo {
+ name: "Unknown Device".to_string(),
+ bus: 0x0003,
+ vendor: 0x9999,
+ product: 0x8888,
+ version: 0x0001,
+ };
+
+ // Unknown key code should return None
+ let scan_code = manager.find_scan_code_for_key(&device, 99999).unwrap();
+ assert_eq!(scan_code, None);
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/key_layout_map_test.rs b/evdev/src/main/rust/evdev_manager/core/tests/key_layout_map_test.rs
new file mode 100644
index 0000000000..38e62f2471
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/key_layout_map_test.rs
@@ -0,0 +1,309 @@
+//! Integration tests for key layout map parsing.
+use evdev_manager_core::android::android_codes::{AKEYCODE_F4, AKEYCODE_FUNCTION};
+use evdev_manager_core::android::keylayout::key_layout_map;
+use evdev_manager_core::android::keylayout::key_layout_map::{KeyLayoutAxisMode, KeyLayoutMap};
+use glob::glob;
+#[cfg(test)]
+use pretty_assertions::assert_eq;
+use std::path::{Path, PathBuf};
+
+fn get_test_data_dir() -> &'static str {
+ env!("CARGO_MANIFEST_DIR")
+}
+
+fn get_test_data_path() -> String {
+ format!("{}/tests/test_data", get_test_data_dir())
+}
+
+fn load_key_layout_map(file_name: &str) -> KeyLayoutMap {
+ let file_path = PathBuf::new().join(get_test_data_path()).join(file_name);
+ KeyLayoutMap::load_from_file(file_path).unwrap()
+}
+
+#[test]
+fn test_parse_int() {
+ assert_eq!(key_layout_map::parse_int("123"), Some(123));
+ assert_eq!(key_layout_map::parse_int("-999"), Some(-999));
+ assert_eq!(key_layout_map::parse_int("0x1a"), Some(26));
+ assert_eq!(key_layout_map::parse_int("0XFF"), Some(255));
+ assert_eq!(key_layout_map::parse_int("077"), Some(63)); // octal
+ assert_eq!(key_layout_map::parse_int(""), None);
+ assert_eq!(key_layout_map::parse_int("abc"), None);
+}
+
+#[test]
+fn test_load_from_contents_simple() {
+ let contents = "key 1 ESCAPE\nkey 2 1\n";
+ let map = KeyLayoutMap::load_from_contents(contents).unwrap();
+
+ let key_code = map.map_key(1).unwrap();
+ assert_eq!(key_code, 111); // ESCAPE
+
+ let key_code = map.map_key(2).unwrap();
+ assert_eq!(key_code, 8); // KEYCODE_1
+}
+
+#[test]
+fn test_load_from_contents_with_flags() {
+ // Flags are parsed but ignored - we just verify the key code is correct
+ let contents = "key 465 ESCAPE FUNCTION\n";
+ let map = KeyLayoutMap::load_from_contents(contents).unwrap();
+
+ let key_code = map.map_key(465).unwrap();
+ assert_eq!(key_code, 111); // ESCAPE
+}
+
+#[test]
+fn test_load_from_contents_axis() {
+ let contents = "axis 0x00 X\naxis 0x01 Y\n";
+ let map = KeyLayoutMap::load_from_contents(contents).unwrap();
+
+ let axis_info = map.map_axis(0x00).unwrap();
+ assert_eq!(axis_info.mode, KeyLayoutAxisMode::Normal);
+ assert_eq!(axis_info.axis, 0); // X axis
+
+ let axis_info = map.map_axis(0x01).unwrap();
+ assert_eq!(axis_info.mode, KeyLayoutAxisMode::Normal);
+ assert_eq!(axis_info.axis, 1); // Y axis
+}
+
+#[test]
+fn test_function_key_line() {
+ let content = "key 464 FUNCTION";
+ let map = KeyLayoutMap::load_from_contents(content).unwrap();
+ let key_code = map.map_key(464).unwrap();
+ assert_eq!(key_code, AKEYCODE_FUNCTION);
+}
+
+#[test]
+fn test_f4_key_line() {
+ let content = "key 469 F4 FUNCTION";
+ let map = KeyLayoutMap::load_from_contents(content).unwrap();
+ let key_code = map.map_key(469).unwrap();
+ assert_eq!(key_code, AKEYCODE_F4);
+}
+
+#[test]
+fn test_brightness_usage_key_lines() {
+ let content = "\
+ key usage 0x0c0067 WINDOW
+ key usage 0x0c006F BRIGHTNESS_UP
+ key usage 0x0c0070 BRIGHTNESS_DOWN";
+
+ KeyLayoutMap::load_from_contents(content).unwrap();
+ // Just do not crash because it should be skipped.
+}
+
+#[test]
+fn test_load_generic_kl() {
+ let map = load_key_layout_map("Generic.kl");
+
+ // Test some basic key mappings
+ let key_code = map.map_key(1).expect("Scan code 1 should map to ESCAPE");
+ assert_eq!(key_code, 111); // ESCAPE
+
+ let key_code = map.map_key(2).expect("Scan code 2 should map to 1");
+ assert_eq!(key_code, 8); // KEYCODE_1
+
+ let key_code = map.map_key(15).expect("Scan code 15 should map to TAB");
+ assert_eq!(key_code, 61); // TAB
+}
+
+#[test]
+fn test_parse_all_key_entries() {
+ let map = load_key_layout_map("Generic.kl");
+
+ // Test various key types
+ let test_cases = vec![
+ (1, 111), // ESCAPE
+ (15, 61), // TAB
+ (28, 66), // ENTER
+ (57, 62), // SPACE
+ (59, 131), // F1
+ (70, 116), // SCROLL_LOCK
+ ];
+
+ for (scan_code, expected_key_code) in test_cases {
+ let key_code = map
+ .map_key(scan_code)
+ .unwrap_or_else(|| panic!("Scan code {} should be mapped", scan_code));
+ assert_eq!(
+ key_code, expected_key_code,
+ "Scan code {} should map to key code {}",
+ scan_code, expected_key_code
+ );
+ }
+}
+
+#[test]
+fn test_parse_axis_entries() {
+ let map = load_key_layout_map("Generic.kl");
+
+ // Test axis mappings
+ let axis_info = map.map_axis(0x00).expect("Axis 0x00 should exist");
+ assert_eq!(axis_info.axis, 0); // X axis
+ assert_eq!(axis_info.mode, KeyLayoutAxisMode::Normal);
+
+ let axis_info = map.map_axis(0x01).expect("Axis 0x01 should exist");
+ assert_eq!(axis_info.axis, 1); // Y axis
+
+ let axis_info = map.map_axis(0x03).expect("Axis 0x03 should exist");
+ assert_eq!(axis_info.axis, 12); // RX axis
+}
+
+#[test]
+fn test_error_handling_malformed_file() {
+ // Test with invalid key entry
+ let contents = "key invalid ESCAPE\n";
+ let result = KeyLayoutMap::load_from_contents(contents);
+ assert!(result.is_err(), "Should fail to parse invalid scan code");
+
+ // Test with missing key code
+ let contents = "key 1\n";
+ let result = KeyLayoutMap::load_from_contents(contents);
+ // This should succeed but the key won't be inserted (unknown key code)
+ assert!(result.is_ok());
+
+ // Test with duplicate scan code
+ let contents = "key 1 ESCAPE\nkey 1 TAB\n";
+ let result = KeyLayoutMap::load_from_contents(contents);
+ assert!(result.is_err(), "Should fail on duplicate scan code");
+}
+
+#[test]
+fn test_map_key_with_scan_codes() {
+ let map = load_key_layout_map("Generic.kl");
+
+ // Test mapping various scan codes
+ let key_code = map.map_key(1).unwrap();
+ assert_eq!(key_code, 111); // ESCAPE
+
+ // Test a key with FUNCTION flag (flags are now ignored)
+ let key_code = map.map_key(465).unwrap();
+ assert_eq!(key_code, 111); // ESCAPE
+
+ // Test that non-existent scan code returns None
+ assert!(map.map_key(9999).is_none());
+}
+
+#[test]
+fn test_map_axis_functionality() {
+ let map = load_key_layout_map("Generic.kl");
+
+ // Test normal axis
+ let axis_info = map.map_axis(0x00).unwrap();
+ assert_eq!(axis_info.mode, KeyLayoutAxisMode::Normal);
+ assert_eq!(axis_info.axis, 0);
+ assert_eq!(axis_info.high_axis, None);
+ assert_eq!(axis_info.split_value, None);
+ assert_eq!(axis_info.flat_override, None);
+
+ // Test that non-existent axis returns None
+ assert!(map.map_axis(9999).is_none());
+}
+
+#[test]
+fn test_find_scan_code_for_key() {
+ let map = load_key_layout_map("Generic.kl");
+
+ // Find scan code for ESCAPE - returns first scan code (1, not 465 with FUNCTION flag)
+ let scan_code = map.find_scan_code_for_key(111); // ESCAPE
+ assert_eq!(scan_code, Some(1), "Should find scan code 1 for ESCAPE");
+
+ // Find scan code for a common key
+ let scan_code = map.find_scan_code_for_key(8); // KEYCODE_1
+ assert_eq!(scan_code, Some(2), "Should find scan code 2 for KEYCODE_1");
+
+ // Non-existent key code should return None
+ assert!(map.find_scan_code_for_key(9999).is_none());
+}
+
+#[test]
+fn test_reverse_mapping_basic() {
+ let contents = "key 1 ESCAPE\nkey 2 1\nkey 100 ESCAPE\n";
+ let map = KeyLayoutMap::load_from_contents(contents).unwrap();
+
+ // ESCAPE (111) returns the first scan code (1)
+ let scan_code = map.find_scan_code_for_key(111);
+ assert_eq!(
+ scan_code,
+ Some(1),
+ "Should return first scan code for ESCAPE"
+ );
+
+ // KEYCODE_1 (8) should have scan code 2
+ let scan_code = map.find_scan_code_for_key(8);
+ assert_eq!(
+ scan_code,
+ Some(2),
+ "Should return scan code 2 for KEYCODE_1"
+ );
+
+ // Non-existent key code should return None
+ assert!(
+ map.find_scan_code_for_key(9999).is_none(),
+ "Non-existent key code should return None"
+ );
+}
+
+#[test]
+fn test_reverse_mapping_keeps_first_scan_code() {
+ // When multiple scan codes map to the same key code, we keep the first one
+ let contents = "key 1 ESCAPE\nkey 465 ESCAPE FUNCTION\n";
+ let map = KeyLayoutMap::load_from_contents(contents).unwrap();
+
+ // Should return the first scan code (1), not 465
+ let scan_code = map.find_scan_code_for_key(111);
+ assert_eq!(
+ scan_code,
+ Some(1),
+ "Should return first scan code for ESCAPE"
+ );
+}
+
+#[test]
+fn test_bidirectional_mapping() {
+ let contents = "key 1 ESCAPE\nkey 2 1\nkey 28 ENTER\n";
+ let map = KeyLayoutMap::load_from_contents(contents).unwrap();
+
+ // Forward mapping
+ assert_eq!(map.map_key(1), Some(111)); // ESCAPE
+ assert_eq!(map.map_key(2), Some(8)); // KEYCODE_1
+ assert_eq!(map.map_key(28), Some(66)); // ENTER
+
+ // Reverse mapping
+ assert_eq!(map.find_scan_code_for_key(111), Some(1)); // ESCAPE -> 1
+ assert_eq!(map.find_scan_code_for_key(8), Some(2)); // KEYCODE_1 -> 2
+ assert_eq!(map.find_scan_code_for_key(66), Some(28)); // ENTER -> 28
+}
+
+#[test]
+fn test_load_all_files_in_test_data() {
+ let test_data_path = get_test_data_path();
+ let test_data_dir = Path::new(&test_data_path);
+ if !test_data_dir.exists() {
+ panic!("Test data directory does not exist: {}", test_data_path);
+ }
+
+ // Use glob pattern to recursively find all .kl files
+ let pattern = format!("{}/**/*.kl", test_data_path);
+ let entries = glob(&pattern).expect("Failed to read glob pattern");
+
+ for entry in entries {
+ let path = entry.expect("Failed to read glob entry");
+ println!("Testing file: {:?}", path.clone());
+
+ let result = KeyLayoutMap::load_from_file(path.clone());
+ assert!(
+ result.is_ok(),
+ "Failed to load key layout file: {:?} - {:?}",
+ path.clone(),
+ result.err()
+ );
+
+ // At least verify it's not empty
+ result.unwrap();
+ // We can't check exact counts as files may vary
+ println!(" Loaded successfully");
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/6t/gpio-keys.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/6t/gpio-keys.kl
new file mode 100644
index 0000000000..f8dbf8577f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/6t/gpio-keys.kl
@@ -0,0 +1,40 @@
+# Copyright (c) 2013, The Linux Foundation. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of The Linux Foundation nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+key 115 VOLUME_UP
+key 114 VOLUME_DOWN
+key 102 HOME
+key 528 FOCUS
+key 766 CAMERA
+
+# ifdef ODM_HQ_EDIT
+# Add for N1 by yutieshuan at 2020/06/13 start
+key 752 RF_CABLE_MID_OUT WAKE_DROPPED
+key 753 RF_CABLE_MID_IN WAKE_DROPPED
+key 754 RF_CABLE_HD_OUT WAKE_DROPPED
+key 755 RF_CABLE_HD_IN WAKE_DROPPED
+# endif
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/Generic.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/Generic.kl
new file mode 100644
index 0000000000..31092536ba
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/Generic.kl
@@ -0,0 +1,482 @@
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Generic key layout file for full alphabetic US English PC style external keyboards.
+#
+# This file is intentionally very generic and is intended to support a broad range of keyboards.
+# Do not edit the generic key layout to support a specific keyboard; instead, create
+# a new key layout file with the required keyboard configuration.
+#
+
+key 1 ESCAPE
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+key 12 MINUS
+key 13 EQUALS
+key 14 DEL
+key 15 TAB
+key 16 Q
+key 17 W
+key 18 E
+key 19 R
+key 20 T
+key 21 Y
+key 22 U
+key 23 I
+key 24 O
+key 25 P
+key 26 LEFT_BRACKET
+key 27 RIGHT_BRACKET
+key 28 ENTER
+key 29 CTRL_LEFT
+key 30 A
+key 31 S
+key 32 D
+key 33 F
+key 34 G
+key 35 H
+key 36 J
+key 37 K
+key 38 L
+key 39 SEMICOLON
+key 40 APOSTROPHE
+key 41 GRAVE
+key 42 SHIFT_LEFT
+key 43 BACKSLASH
+key 44 Z
+key 45 X
+key 46 C
+key 47 V
+key 48 B
+key 49 N
+key 50 M
+key 51 COMMA
+key 52 PERIOD
+key 53 SLASH
+key 54 SHIFT_RIGHT
+key 55 NUMPAD_MULTIPLY
+key 56 ALT_LEFT
+key 57 SPACE
+key 58 CAPS_LOCK
+key 59 F1
+key 60 F2
+key 61 F3
+key 62 F4
+key 63 F5
+key 64 F6
+key 65 F7
+key 66 F8
+key 67 F9
+key 68 F10
+key 69 NUM_LOCK
+key 70 SCROLL_LOCK
+key 71 NUMPAD_7
+key 72 NUMPAD_8
+key 73 NUMPAD_9
+key 74 NUMPAD_SUBTRACT
+key 75 NUMPAD_4
+key 76 NUMPAD_5
+key 77 NUMPAD_6
+key 78 NUMPAD_ADD
+key 79 NUMPAD_1
+key 80 NUMPAD_2
+key 81 NUMPAD_3
+key 82 NUMPAD_0
+key 83 NUMPAD_DOT
+# key 84 (undefined)
+key 85 ZENKAKU_HANKAKU
+key 86 BACKSLASH
+key 87 F11
+key 88 F12
+key 89 RO
+# key 90 "KEY_KATAKANA"
+# key 91 "KEY_HIRAGANA"
+key 92 HENKAN
+key 93 KATAKANA_HIRAGANA
+key 94 MUHENKAN
+key 95 NUMPAD_COMMA
+key 96 NUMPAD_ENTER
+key 97 CTRL_RIGHT
+key 98 NUMPAD_DIVIDE
+key 99 SYSRQ
+key 100 ALT_RIGHT
+# key 101 "KEY_LINEFEED"
+key 102 MOVE_HOME
+key 103 DPAD_UP
+key 104 PAGE_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 107 MOVE_END
+key 108 DPAD_DOWN
+key 109 PAGE_DOWN
+key 110 INSERT
+key 111 FORWARD_DEL
+# key 112 "KEY_MACRO"
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+key 116 POWER
+key 117 NUMPAD_EQUALS
+# key 118 "KEY_KPPLUSMINUS"
+key 119 BREAK
+key 120 RECENT_APPS
+key 121 NUMPAD_COMMA
+key 122 KANA
+key 123 EISU
+key 124 YEN
+key 125 META_LEFT
+key 126 META_RIGHT
+key 127 MENU
+key 128 MEDIA_STOP
+# key 129 "KEY_AGAIN"
+# key 130 "KEY_PROPS"
+# key 131 "KEY_UNDO"
+# key 132 "KEY_FRONT"
+key 133 COPY
+# key 134 "KEY_OPEN"
+key 135 PASTE
+# key 136 "KEY_FIND"
+key 137 CUT
+# key 138 "KEY_HELP"
+key 139 MENU
+key 140 CALCULATOR
+# key 141 "KEY_SETUP"
+key 142 SLEEP
+key 143 WAKEUP
+# key 144 "KEY_FILE"
+# key 145 "KEY_SENDFILE"
+# key 146 "KEY_DELETEFILE"
+# key 147 "KEY_XFER"
+# key 148 "KEY_PROG1"
+# key 149 "KEY_PROG2"
+key 150 EXPLORER
+# key 151 "KEY_MSDOS"
+key 152 POWER
+# key 153 "KEY_DIRECTION"
+# key 154 "KEY_CYCLEWINDOWS"
+key 155 ENVELOPE
+key 156 BOOKMARK
+# key 157 "KEY_COMPUTER"
+key 158 BACK
+key 159 FORWARD
+key 160 MEDIA_CLOSE
+key 161 MEDIA_EJECT
+key 162 MEDIA_EJECT
+key 163 MEDIA_NEXT
+key 164 MEDIA_PLAY_PAUSE
+key 165 MEDIA_PREVIOUS
+key 166 MEDIA_STOP
+key 167 MEDIA_RECORD
+key 168 MEDIA_REWIND
+key 169 CALL
+# key 170 "KEY_ISO"
+key 171 MUSIC
+key 172 HOME
+key 173 REFRESH
+# key 174 "KEY_EXIT"
+# key 175 "KEY_MOVE"
+# key 176 "KEY_EDIT"
+key 177 PAGE_UP
+key 178 PAGE_DOWN
+key 179 NUMPAD_LEFT_PAREN
+key 180 NUMPAD_RIGHT_PAREN
+# key 181 "KEY_NEW"
+# key 182 "KEY_REDO"
+# key 183 F13
+# key 184 F14
+# key 185 F15
+# key 186 F16
+# key 187 F17
+# key 188 F18
+# key 189 F19
+# key 190 F20
+# key 191 F21
+# key 192 F22
+# key 193 F23
+# key 194 F24
+# key 195 (undefined)
+# key 196 (undefined)
+# key 197 (undefined)
+# key 198 (undefined)
+# key 199 (undefined)
+key 200 MEDIA_PLAY
+key 201 MEDIA_PAUSE
+# key 202 "KEY_PROG3"
+# key 203 "KEY_PROG4"
+key 204 NOTIFICATION
+# key 205 "KEY_SUSPEND"
+# key 206 "KEY_CLOSE"
+key 207 MEDIA_PLAY
+key 208 MEDIA_FAST_FORWARD
+# key 209 "KEY_BASSBOOST"
+# key 210 "KEY_PRINT"
+# key 211 "KEY_HP"
+key 212 CAMERA
+key 213 MUSIC
+# key 214 "KEY_QUESTION"
+key 215 ENVELOPE
+# key 216 "KEY_CHAT"
+key 217 SEARCH
+# key 218 "KEY_CONNECT"
+# key 219 "KEY_FINANCE"
+# key 220 "KEY_SPORT"
+# key 221 "KEY_SHOP"
+# key 222 "KEY_ALTERASE"
+# key 223 "KEY_CANCEL"
+key 224 BRIGHTNESS_DOWN
+key 225 BRIGHTNESS_UP
+key 226 HEADSETHOOK
+key 228 KEYBOARD_BACKLIGHT_TOGGLE
+key 229 KEYBOARD_BACKLIGHT_DOWN
+key 230 KEYBOARD_BACKLIGHT_UP
+key 248 MUTE
+
+key 256 BUTTON_1
+key 257 BUTTON_2
+key 258 BUTTON_3
+key 259 BUTTON_4
+key 260 BUTTON_5
+key 261 BUTTON_6
+key 262 BUTTON_7
+key 263 BUTTON_8
+key 264 BUTTON_9
+key 265 BUTTON_10
+key 266 BUTTON_11
+key 267 BUTTON_12
+key 268 BUTTON_13
+key 269 BUTTON_14
+key 270 BUTTON_15
+key 271 BUTTON_16
+
+key 288 BUTTON_1
+key 289 BUTTON_2
+key 290 BUTTON_3
+key 291 BUTTON_4
+key 292 BUTTON_5
+key 293 BUTTON_6
+key 294 BUTTON_7
+key 295 BUTTON_8
+key 296 BUTTON_9
+key 297 BUTTON_10
+key 298 BUTTON_11
+key 299 BUTTON_12
+key 300 BUTTON_13
+key 301 BUTTON_14
+key 302 BUTTON_15
+key 303 BUTTON_16
+
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 306 BUTTON_C
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 309 BUTTON_Z
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 312 BUTTON_L2
+key 313 BUTTON_R2
+key 314 BUTTON_SELECT
+key 315 BUTTON_START
+key 316 BUTTON_MODE
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+
+key 329 STYLUS_BUTTON_TERTIARY
+key 331 STYLUS_BUTTON_PRIMARY
+key 332 STYLUS_BUTTON_SECONDARY
+
+
+# key 352 "KEY_OK"
+key 353 DPAD_CENTER
+# key 354 "KEY_GOTO"
+# key 355 "KEY_CLEAR"
+# key 356 "KEY_POWER2"
+# key 357 "KEY_OPTION"
+# key 358 "KEY_INFO"
+# key 359 "KEY_TIME"
+# key 360 "KEY_VENDOR"
+# key 361 "KEY_ARCHIVE"
+key 362 GUIDE
+# key 363 "KEY_CHANNEL"
+# key 364 "KEY_FAVORITES"
+# key 365 "KEY_EPG"
+key 366 DVR
+# key 367 "KEY_MHP"
+# key 368 "KEY_LANGUAGE"
+# key 369 "KEY_TITLE"
+key 370 CAPTIONS
+# key 371 "KEY_ANGLE"
+# key 372 "KEY_ZOOM"
+# key 373 "KEY_MODE"
+# key 374 "KEY_KEYBOARD"
+# key 375 "KEY_SCREEN"
+# key 376 "KEY_PC"
+key 377 TV
+# key 378 "KEY_TV2"
+# key 379 "KEY_VCR"
+# key 380 "KEY_VCR2"
+# key 381 "KEY_SAT"
+# key 382 "KEY_SAT2"
+# key 383 "KEY_CD"
+# key 384 "KEY_TAPE"
+# key 385 "KEY_RADIO"
+# key 386 "KEY_TUNER"
+# key 387 "KEY_PLAYER"
+# key 388 "KEY_TEXT"
+# key 389 "KEY_DVD"
+# key 390 "KEY_AUX"
+# key 391 "KEY_MP3"
+# key 392 "KEY_AUDIO"
+# key 393 "KEY_VIDEO"
+# key 394 "KEY_DIRECTORY"
+# key 395 "KEY_LIST"
+# key 396 "KEY_MEMO"
+key 397 CALENDAR
+key 398 PROG_RED
+key 399 PROG_GREEN
+key 400 PROG_YELLOW
+key 401 PROG_BLUE
+key 402 CHANNEL_UP
+key 403 CHANNEL_DOWN
+# key 404 "KEY_FIRST"
+key 405 LAST_CHANNEL
+# key 406 "KEY_AB"
+# key 407 "KEY_NEXT"
+# key 408 "KEY_RESTART"
+# key 409 "KEY_SLOW"
+# key 410 "KEY_SHUFFLE"
+# key 411 "KEY_BREAK"
+# key 412 "KEY_PREVIOUS"
+# key 413 "KEY_DIGITS"
+# key 414 "KEY_TEEN"
+# key 415 "KEY_TWEN"
+# key 418 "KEY_ZOOM_IN"
+key 418 ZOOM_IN
+# key 419 "KEY_ZOOM_OUT"
+key 419 ZOOM_OUT
+key 528 FOCUS
+
+key 429 CONTACTS
+
+# key 448 "KEY_DEL_EOL"
+# key 449 "KEY_DEL_EOS"
+# key 450 "KEY_INS_LINE"
+# key 451 "KEY_DEL_LINE"
+
+
+key 464 FUNCTION
+key 465 ESCAPE FUNCTION
+key 466 F1 FUNCTION
+key 467 F2 FUNCTION
+key 468 F3 FUNCTION
+key 469 F4 FUNCTION
+key 470 F5 FUNCTION
+key 471 F6 FUNCTION
+key 472 F7 FUNCTION
+key 473 F8 FUNCTION
+key 474 F9 FUNCTION
+key 475 F10 FUNCTION
+key 476 F11 FUNCTION
+key 477 F12 FUNCTION
+key 478 1 FUNCTION
+key 479 2 FUNCTION
+key 480 D FUNCTION
+key 481 E FUNCTION
+key 482 F FUNCTION
+key 483 S FUNCTION
+key 484 B FUNCTION
+
+
+# key 497 KEY_BRL_DOT1
+# key 498 KEY_BRL_DOT2
+# key 499 KEY_BRL_DOT3
+# key 500 KEY_BRL_DOT4
+# key 501 KEY_BRL_DOT5
+# key 502 KEY_BRL_DOT6
+# key 503 KEY_BRL_DOT7
+# key 504 KEY_BRL_DOT8
+
+key 522 STAR
+key 523 POUND
+key 580 APP_SWITCH
+key 582 VOICE_ASSIST
+# Linux KEY_ASSISTANT
+key 583 ASSIST
+key 656 MACRO_1
+key 657 MACRO_2
+key 658 MACRO_3
+key 659 MACRO_4
+
+# Keys defined by HID usages
+key usage 0x0c0067 WINDOW
+key usage 0x0c006F BRIGHTNESS_UP
+key usage 0x0c0070 BRIGHTNESS_DOWN
+key usage 0x0c0079 KEYBOARD_BACKLIGHT_UP
+key usage 0x0c007A KEYBOARD_BACKLIGHT_DOWN
+key usage 0x0c007C KEYBOARD_BACKLIGHT_TOGGLE
+key usage 0x0c0173 MEDIA_AUDIO_TRACK
+key usage 0x0c019C PROFILE_SWITCH
+key usage 0x0c01A2 ALL_APPS
+# TODO(b/297094448): Add stylus button mappings as a fallback when we have a way to determine
+# if a device can actually report it.
+# key usage 0x0d0044 STYLUS_BUTTON_PRIMARY
+# key usage 0x0d005a STYLUS_BUTTON_SECONDARY
+
+# Joystick and game controller axes.
+# Axes that are not mapped will be assigned generic axis numbers by the input subsystem.
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x03 RX
+axis 0x04 RY
+axis 0x05 RZ
+axis 0x06 THROTTLE
+axis 0x07 RUDDER
+axis 0x08 WHEEL
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# LEDs
+led 0x00 NUM_LOCK
+led 0x01 CAPS_LOCK
+led 0x02 SCROLL_LOCK
+led 0x03 COMPOSE
+led 0x04 KANA
+led 0x05 SLEEP
+led 0x06 SUSPEND
+led 0x07 MUTE
+led 0x08 MISC
+led 0x09 MAIL
+led 0x0a CHARGING
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/AVRCP.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/AVRCP.kl
new file mode 100644
index 0000000000..ccd0209a73
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/AVRCP.kl
@@ -0,0 +1,23 @@
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Key layout used for Bluetooth AVRCP support.
+
+key 200 MEDIA_PLAY
+key 201 MEDIA_PAUSE
+key 166 MEDIA_STOP
+key 163 MEDIA_NEXT
+key 165 MEDIA_PREVIOUS
+key 168 MEDIA_REWIND
+key 208 MEDIA_FAST_FORWARD
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Generic.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Generic.kl
new file mode 100644
index 0000000000..2b0802b54c
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Generic.kl
@@ -0,0 +1,496 @@
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Generic key layout file for full alphabetic US English PC style external keyboards.
+#
+# This file is intentionally very generic and is intended to support a broad range of keyboards.
+# Do not edit the generic key layout to support a specific keyboard; instead, create
+# a new key layout file with the required keyboard configuration.
+#
+
+key 1 ESCAPE
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+key 12 MINUS
+key 13 EQUALS
+key 14 DEL
+key 15 TAB
+key 16 Q
+key 17 W
+key 18 E
+key 19 R
+key 20 T
+key 21 Y
+key 22 U
+key 23 I
+key 24 O
+key 25 P
+key 26 LEFT_BRACKET
+key 27 RIGHT_BRACKET
+key 28 ENTER
+key 29 CTRL_LEFT
+key 30 A
+key 31 S
+key 32 D
+key 33 F
+key 34 G
+key 35 H
+key 36 J
+key 37 K
+key 38 L
+key 39 SEMICOLON
+key 40 APOSTROPHE
+key 41 GRAVE
+key 42 SHIFT_LEFT
+key 43 BACKSLASH
+key 44 Z
+key 45 X
+key 46 C
+key 47 V
+key 48 B
+key 49 N
+key 50 M
+key 51 COMMA
+key 52 PERIOD
+key 53 SLASH
+key 54 SHIFT_RIGHT
+key 55 NUMPAD_MULTIPLY
+key 56 ALT_LEFT
+key 57 SPACE
+key 58 CAPS_LOCK
+key 59 F1
+key 60 F2
+key 61 F3
+key 62 F4
+key 63 F5
+key 64 F6
+key 65 F7
+key 66 F8
+key 67 F9
+key 68 F10
+key 69 NUM_LOCK
+key 70 SCROLL_LOCK
+key 71 NUMPAD_7
+key 72 NUMPAD_8
+key 73 NUMPAD_9
+key 74 NUMPAD_SUBTRACT
+key 75 NUMPAD_4
+key 76 NUMPAD_5
+key 77 NUMPAD_6
+key 78 NUMPAD_ADD
+key 79 NUMPAD_1
+key 80 NUMPAD_2
+key 81 NUMPAD_3
+key 82 NUMPAD_0
+key 83 NUMPAD_DOT
+# key 84 (undefined)
+key 85 ZENKAKU_HANKAKU
+key 86 BACKSLASH
+key 87 F11
+key 88 F12
+key 89 RO
+# key 90 "KEY_KATAKANA"
+# key 91 "KEY_HIRAGANA"
+key 92 HENKAN
+key 93 KATAKANA_HIRAGANA
+key 94 MUHENKAN
+key 95 NUMPAD_COMMA
+key 96 NUMPAD_ENTER
+key 97 CTRL_RIGHT
+key 98 NUMPAD_DIVIDE
+key 99 SYSRQ
+key 100 ALT_RIGHT
+# key 101 "KEY_LINEFEED"
+key 102 MOVE_HOME
+key 103 DPAD_UP
+key 104 PAGE_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 107 MOVE_END
+key 108 DPAD_DOWN
+key 109 PAGE_DOWN
+key 110 INSERT
+key 111 FORWARD_DEL
+# key 112 "KEY_MACRO"
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+key 116 POWER
+key 117 NUMPAD_EQUALS
+# key 118 "KEY_KPPLUSMINUS"
+key 119 BREAK
+key 120 RECENT_APPS
+key 121 NUMPAD_COMMA
+key 122 KANA
+key 123 EISU
+key 124 YEN
+key 125 META_LEFT
+key 126 META_RIGHT
+key 127 MENU
+key 128 MEDIA_STOP
+# key 129 "KEY_AGAIN"
+# key 130 "KEY_PROPS"
+# key 131 "KEY_UNDO"
+# key 132 "KEY_FRONT"
+key 133 COPY
+# key 134 "KEY_OPEN"
+key 135 PASTE
+# key 136 "KEY_FIND"
+key 137 CUT
+# key 138 "KEY_HELP"
+key 139 MENU
+key 140 CALCULATOR
+# key 141 "KEY_SETUP"
+key 142 SLEEP
+key 143 WAKEUP
+# key 144 "KEY_FILE"
+# key 145 "KEY_SENDFILE"
+# key 146 "KEY_DELETEFILE"
+# key 147 "KEY_XFER"
+# key 148 "KEY_PROG1"
+# key 149 "KEY_PROG2"
+key 150 EXPLORER
+# key 151 "KEY_MSDOS"
+key 152 LOCK
+# key 153 "KEY_DIRECTION"
+# key 154 "KEY_CYCLEWINDOWS"
+key 155 ENVELOPE
+key 156 BOOKMARK
+# key 157 "KEY_COMPUTER"
+key 158 BACK
+key 159 FORWARD
+key 160 MEDIA_CLOSE
+key 161 MEDIA_EJECT
+key 162 MEDIA_EJECT
+key 163 MEDIA_NEXT
+key 164 MEDIA_PLAY_PAUSE
+key 165 MEDIA_PREVIOUS
+key 166 MEDIA_STOP
+key 167 MEDIA_RECORD
+key 168 MEDIA_REWIND
+key 169 CALL
+# key 170 "KEY_ISO"
+key 171 MUSIC
+key 172 HOME
+key 173 REFRESH
+# key 174 "KEY_EXIT"
+# key 175 "KEY_MOVE"
+# key 176 "KEY_EDIT"
+key 177 PAGE_UP
+key 178 PAGE_DOWN
+key 179 NUMPAD_LEFT_PAREN
+key 180 NUMPAD_RIGHT_PAREN
+key 181 NEW
+# key 182 "KEY_REDO"
+key 183 F13
+key 184 F14
+key 185 F15
+key 186 F16
+key 187 F17
+key 188 F18
+key 189 F19
+key 190 F20
+key 191 F21
+key 192 F22
+key 193 F23
+key 194 F24
+# key 195 (undefined)
+# key 196 (undefined)
+# key 197 (undefined)
+# key 198 (undefined)
+# key 199 (undefined)
+key 200 MEDIA_PLAY
+key 201 MEDIA_PAUSE
+# key 202 "KEY_PROG3"
+# key 203 "KEY_PROG4"
+key 204 NOTIFICATION
+# key 205 "KEY_SUSPEND"
+key 206 CLOSE
+key 207 MEDIA_PLAY
+key 208 MEDIA_FAST_FORWARD
+# key 209 "KEY_BASSBOOST"
+key 210 PRINT
+# key 211 "KEY_HP"
+key 212 CAMERA
+key 213 MUSIC
+# key 214 "KEY_QUESTION"
+key 215 ENVELOPE
+# key 216 "KEY_CHAT"
+key 217 SEARCH
+# key 218 "KEY_CONNECT"
+# key 219 "KEY_FINANCE"
+# key 220 "KEY_SPORT"
+# key 221 "KEY_SHOP"
+# key 222 "KEY_ALTERASE"
+# key 223 "KEY_CANCEL"
+key 224 BRIGHTNESS_DOWN
+key 225 BRIGHTNESS_UP
+key 226 HEADSETHOOK
+key 228 KEYBOARD_BACKLIGHT_TOGGLE
+key 229 KEYBOARD_BACKLIGHT_DOWN
+key 230 KEYBOARD_BACKLIGHT_UP
+key 248 MUTE
+
+key 256 BUTTON_1
+key 257 BUTTON_2
+key 258 BUTTON_3
+key 259 BUTTON_4
+key 260 BUTTON_5
+key 261 BUTTON_6
+key 262 BUTTON_7
+key 263 BUTTON_8
+key 264 BUTTON_9
+key 265 BUTTON_10
+key 266 BUTTON_11
+key 267 BUTTON_12
+key 268 BUTTON_13
+key 269 BUTTON_14
+key 270 BUTTON_15
+key 271 BUTTON_16
+
+key 288 BUTTON_1
+key 289 BUTTON_2
+key 290 BUTTON_3
+key 291 BUTTON_4
+key 292 BUTTON_5
+key 293 BUTTON_6
+key 294 BUTTON_7
+key 295 BUTTON_8
+key 296 BUTTON_9
+key 297 BUTTON_10
+key 298 BUTTON_11
+key 299 BUTTON_12
+key 300 BUTTON_13
+key 301 BUTTON_14
+key 302 BUTTON_15
+key 303 BUTTON_16
+
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 306 BUTTON_C
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 309 BUTTON_Z
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 312 BUTTON_L2
+key 313 BUTTON_R2
+key 314 BUTTON_SELECT
+key 315 BUTTON_START
+key 316 BUTTON_MODE
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+
+key 329 STYLUS_BUTTON_TERTIARY
+key 331 STYLUS_BUTTON_PRIMARY
+key 332 STYLUS_BUTTON_SECONDARY
+
+
+# key 352 "KEY_OK"
+key 353 DPAD_CENTER
+# key 354 "KEY_GOTO"
+# key 355 "KEY_CLEAR"
+# key 356 "KEY_POWER2"
+# key 357 "KEY_OPTION"
+# key 358 "KEY_INFO"
+# key 359 "KEY_TIME"
+# key 360 "KEY_VENDOR"
+# key 361 "KEY_ARCHIVE"
+key 362 GUIDE
+# key 363 "KEY_CHANNEL"
+# key 364 "KEY_FAVORITES"
+# key 365 "KEY_EPG"
+key 366 DVR
+# key 367 "KEY_MHP"
+key 368 LANGUAGE_SWITCH
+# key 369 "KEY_TITLE"
+key 370 CAPTIONS
+# key 371 "KEY_ANGLE"
+key 372 FULLSCREEN
+# key 373 "KEY_MODE"
+# key 374 "KEY_KEYBOARD"
+# key 375 "KEY_SCREEN"
+# key 376 "KEY_PC"
+key 377 TV
+# key 378 "KEY_TV2"
+# key 379 "KEY_VCR"
+# key 380 "KEY_VCR2"
+# key 381 "KEY_SAT"
+# key 382 "KEY_SAT2"
+# key 383 "KEY_CD"
+# key 384 "KEY_TAPE"
+# key 385 "KEY_RADIO"
+# key 386 "KEY_TUNER"
+# key 387 "KEY_PLAYER"
+# key 388 "KEY_TEXT"
+# key 389 "KEY_DVD"
+# key 390 "KEY_AUX"
+# key 391 "KEY_MP3"
+# key 392 "KEY_AUDIO"
+# key 393 "KEY_VIDEO"
+# key 394 "KEY_DIRECTORY"
+# key 395 "KEY_LIST"
+# key 396 "KEY_MEMO"
+key 397 CALENDAR
+key 398 PROG_RED
+key 399 PROG_GREEN
+key 400 PROG_YELLOW
+key 401 PROG_BLUE
+key 402 CHANNEL_UP
+key 403 CHANNEL_DOWN
+# key 404 "KEY_FIRST"
+key 405 LAST_CHANNEL
+# key 406 "KEY_AB"
+# key 407 "KEY_NEXT"
+# key 408 "KEY_RESTART"
+# key 409 "KEY_SLOW"
+# key 410 "KEY_SHUFFLE"
+# key 411 "KEY_BREAK"
+# key 412 "KEY_PREVIOUS"
+# key 413 "KEY_DIGITS"
+# key 414 "KEY_TEEN"
+# key 415 "KEY_TWEN"
+# key 418 "KEY_ZOOM_IN"
+key 418 ZOOM_IN
+# key 419 "KEY_ZOOM_OUT"
+key 419 ZOOM_OUT
+key 528 FOCUS
+
+key 429 CONTACTS
+
+# key 448 "KEY_DEL_EOL"
+# key 449 "KEY_DEL_EOS"
+# key 450 "KEY_INS_LINE"
+# key 451 "KEY_DEL_LINE"
+
+
+key 464 FUNCTION
+key 465 ESCAPE FUNCTION
+key 466 F1 FUNCTION
+key 467 F2 FUNCTION
+key 468 F3 FUNCTION
+key 469 F4 FUNCTION
+key 470 F5 FUNCTION
+key 471 F6 FUNCTION
+key 472 F7 FUNCTION
+key 473 F8 FUNCTION
+key 474 F9 FUNCTION
+key 475 F10 FUNCTION
+key 476 F11 FUNCTION
+key 477 F12 FUNCTION
+key 478 1 FUNCTION
+key 479 2 FUNCTION
+key 480 D FUNCTION
+key 481 E FUNCTION
+key 482 F FUNCTION
+key 483 S FUNCTION
+key 484 B FUNCTION
+
+
+# key 497 KEY_BRL_DOT1
+# key 498 KEY_BRL_DOT2
+# key 499 KEY_BRL_DOT3
+# key 500 KEY_BRL_DOT4
+# key 501 KEY_BRL_DOT5
+# key 502 KEY_BRL_DOT6
+# key 503 KEY_BRL_DOT7
+# key 504 KEY_BRL_DOT8
+
+key 522 STAR
+key 523 POUND
+key 580 APP_SWITCH
+key 582 VOICE_ASSIST
+# Linux KEY_ASSISTANT
+key 583 ASSIST
+key 585 EMOJI_PICKER
+key 586 DICTATE
+key 656 MACRO_1
+key 657 MACRO_2
+key 658 MACRO_3
+key 659 MACRO_4
+
+# Keys defined by HID usages
+key usage 0x010082 LOCK FALLBACK_USAGE_MAPPING
+key usage 0x01009B DO_NOT_DISTURB FALLBACK_USAGE_MAPPING
+key usage 0x0c0065 SCREENSHOT FALLBACK_USAGE_MAPPING
+key usage 0x0c0067 WINDOW FALLBACK_USAGE_MAPPING
+key usage 0x0c006F BRIGHTNESS_UP FALLBACK_USAGE_MAPPING
+key usage 0x0c0070 BRIGHTNESS_DOWN FALLBACK_USAGE_MAPPING
+key usage 0x0c0079 KEYBOARD_BACKLIGHT_UP FALLBACK_USAGE_MAPPING
+key usage 0x0c007A KEYBOARD_BACKLIGHT_DOWN FALLBACK_USAGE_MAPPING
+key usage 0x0c007C KEYBOARD_BACKLIGHT_TOGGLE FALLBACK_USAGE_MAPPING
+key usage 0x0c00D8 DICTATE FALLBACK_USAGE_MAPPING
+key usage 0x0c00D9 EMOJI_PICKER FALLBACK_USAGE_MAPPING
+key usage 0x0c0173 MEDIA_AUDIO_TRACK FALLBACK_USAGE_MAPPING
+key usage 0x0c019C PROFILE_SWITCH FALLBACK_USAGE_MAPPING
+key usage 0x0c019F SETTINGS FALLBACK_USAGE_MAPPING
+key usage 0x0c01A2 ALL_APPS FALLBACK_USAGE_MAPPING
+key usage 0x0c0201 NEW FALLBACK_USAGE_MAPPING
+key usage 0x0c0203 CLOSE FALLBACK_USAGE_MAPPING
+key usage 0x0c0208 PRINT FALLBACK_USAGE_MAPPING
+key usage 0x0c0227 REFRESH FALLBACK_USAGE_MAPPING
+key usage 0x0c0232 FULLSCREEN FALLBACK_USAGE_MAPPING
+key usage 0x0c029D LANGUAGE_SWITCH FALLBACK_USAGE_MAPPING
+key usage 0x0c029F RECENT_APPS FALLBACK_USAGE_MAPPING
+key usage 0x0c02A2 ALL_APPS FALLBACK_USAGE_MAPPING
+key usage 0x0d0044 STYLUS_BUTTON_PRIMARY FALLBACK_USAGE_MAPPING
+key usage 0x0d005a STYLUS_BUTTON_SECONDARY FALLBACK_USAGE_MAPPING
+
+# Joystick and game controller axes.
+# Axes that are not mapped will be assigned generic axis numbers by the input subsystem.
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x03 RX
+axis 0x04 RY
+axis 0x05 RZ
+axis 0x06 THROTTLE
+axis 0x07 RUDDER
+axis 0x08 WHEEL
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# LEDs
+led 0x00 NUM_LOCK
+led 0x01 CAPS_LOCK
+led 0x02 SCROLL_LOCK
+led 0x03 COMPOSE
+led 0x04 KANA
+led 0x05 SLEEP
+led 0x06 SUSPEND
+led 0x07 MUTE
+led 0x08 MISC
+led 0x09 MAIL
+led 0x0a CHARGING
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0079_Product_0011.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0079_Product_0011.kl
new file mode 100644
index 0000000000..32f8c829cd
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0079_Product_0011.kl
@@ -0,0 +1,27 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Classic [S]NES Controller
+
+key 288 BUTTON_X
+key 289 BUTTON_A
+key 290 BUTTON_B
+key 291 BUTTON_Y
+key 292 BUTTON_L1
+key 293 BUTTON_R1
+key 297 BUTTON_START
+key 296 BUTTON_SELECT
+
+axis 0x00 HAT_X
+axis 0x01 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0079_Product_18d4.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0079_Product_18d4.kl
new file mode 100644
index 0000000000..b9a2b67afb
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0079_Product_18d4.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# GPD Win 2 X-Box Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0171_Product_0419.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0171_Product_0419.kl
new file mode 100644
index 0000000000..05a25f0210
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0171_Product_0419.kl
@@ -0,0 +1,55 @@
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Amazon Luna Controller
+#
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Left and right stick.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x02 Z flat 4096
+axis 0x05 RZ flat 4096
+
+# Triggers.
+axis 0x0a LTRIGGER
+axis 0x09 RTRIGGER
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+
+# Action button (circle icon, left of the Home button)
+key 158 BUTTON_SELECT
+
+# Home button (branded button in the center of the controller)
+key 172 BUTTON_MODE
+
+# Menu button (hamburger icon, right of the Home button)
+key 315 BUTTON_START
+
+# Alexa Push-To-Talk button (microphone icon, below the Home button)
+key 217 MEDIA_RECORD
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0428_Product_4001.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0428_Product_4001.kl
new file mode 100644
index 0000000000..7d1dd12803
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0428_Product_4001.kl
@@ -0,0 +1,27 @@
+# Gravis GamePad Pro USB
+
+# Yellow
+key 0x131 BUTTON_A
+# Green
+key 0x132 BUTTON_B
+# Red
+key 0x130 BUTTON_X
+# Blue
+key 0x133 BUTTON_Y
+
+# Left Upper Shoulder "1"
+key 0x134 BUTTON_L1
+# Right Upper Shoulder "1"
+key 0x135 BUTTON_R1
+# Left Lower Shoulder "2"
+key 0x136 BUTTON_L2
+# Right Lower Shoulder "2"
+key 0x137 BUTTON_R2
+
+# Select & Start
+key 0x138 BUTTON_SELECT
+key 0x139 BUTTON_START
+
+# D-Pad
+axis 0x00 HAT_X
+axis 0x01 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_044f_Product_b326.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_044f_Product_b326.kl
new file mode 100644
index 0000000000..d248d71356
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_044f_Product_b326.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Thrustmaster Gamepad GP XID
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_028e.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_028e.kl
new file mode 100644
index 0000000000..e4f48f9fb3
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_028e.kl
@@ -0,0 +1,55 @@
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# XBox 360 USB Controller
+#
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+
+# Button labeled as "BACK" (left-pointing triangle)
+key 314 BUTTON_SELECT
+
+# The branded "X" button in the center of the controller
+key 316 BUTTON_MODE
+
+# Button labeled as "START" (right-pointing triangle)
+key 315 BUTTON_START
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_028f.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_028f.kl
new file mode 100644
index 0000000000..cc5b33b9a1
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_028f.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Microsoft X-Box 360 pad v2
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02a1.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02a1.kl
new file mode 100644
index 0000000000..0214361717
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02a1.kl
@@ -0,0 +1,55 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# XBox 360 Wireless Controller
+#
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+
+# Button labeled as "BACK" (left-pointing triangle)
+key 314 BUTTON_SELECT
+
+# The branded "X" button in the center of the controller
+key 316 BUTTON_MODE
+
+# Button labeled as "START" (right-pointing triangle)
+key 315 BUTTON_START
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02d1.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02d1.kl
new file mode 100644
index 0000000000..0867670759
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02d1.kl
@@ -0,0 +1,57 @@
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# XBox One Controller - Model 1537 - USB
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02dd.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02dd.kl
new file mode 100644
index 0000000000..3975cec24f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02dd.kl
@@ -0,0 +1,57 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# XBox One Controller - Model 1697 - USB
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02e0.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02e0.kl
new file mode 100644
index 0000000000..1dd8e157c5
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02e0.kl
@@ -0,0 +1,59 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Xbox Wireless Controller
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 0x132 BUTTON_X
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+
+# LT axis
+axis 0x02 LTRIGGER
+# RT axis
+axis 0x05 RTRIGGER
+
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x138 BUTTON_THUMBL
+# Right stick click
+key 0x139 BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 0x136 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 0x137 BUTTON_START
+
+# Xbox key
+key 0x8b BUTTON_MODE
\ No newline at end of file
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02e3.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02e3.kl
new file mode 100644
index 0000000000..0a6e7d75c2
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02e3.kl
@@ -0,0 +1,56 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Microsoft X-Box One Elite Pad - Model 1698 - USB
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x133 BUTTON_X
+key 0x134 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left stick
+axis 0x00 X
+axis 0x01 Y
+# Right stick
+axis 0x03 Z
+axis 0x04 RZ
+
+key 0x13d BUTTON_THUMBL
+key 0x13e BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+
+# Two overlapping rectangles
+key 0x13a BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 0x13b BUTTON_START
+
+# Xbox key
+key 0x13c BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02ea.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02ea.kl
new file mode 100644
index 0000000000..3b46db2ce9
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02ea.kl
@@ -0,0 +1,57 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# XBox One Controller - Model 1708 - USB
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02fd.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02fd.kl
new file mode 100644
index 0000000000..1b03497ae3
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_02fd.kl
@@ -0,0 +1,62 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# XBox One Controller - Model 1708 - Bluetooth
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x0a LTRIGGER
+axis 0x09 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x02 Z flat 4096
+axis 0x05 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 158 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# There are at least two versions of firmware out for this controller.
+# They send different linux keys for the "Xbox" button.
+# Xbox key (original firmware)
+key 172 BUTTON_MODE
+
+# Xbox key (newer firmware)
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_0b12.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_0b12.kl
new file mode 100644
index 0000000000..0b44c7434a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_045e_Product_0b12.kl
@@ -0,0 +1,59 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# XBox USB Controller
+#
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+
+# The branded "X" button in the center of the controller
+key 316 BUTTON_MODE
+
+# Three parallel horizontal lines (hamburger menu)
+key 315 BUTTON_START
+
+#Button below the "X" button
+key 167 MEDIA_RECORD
+
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_b501.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_b501.kl
new file mode 100644
index 0000000000..496ddc3eec
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_b501.kl
@@ -0,0 +1,65 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Logitech Bluetooth wireless gamepad (RedHawk)
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 312 BUTTON_L2
+key 313 BUTTON_R2
+
+key 314 BUTTON_SELECT
+key 315 BUTTON_START
+key 316 BUTTON_MODE
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+key 103 DPAD_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 108 DPAD_DOWN
+key 353 DPAD_CENTER
+
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+key 152 POWER
+key 163 MEDIA_NEXT
+key 164 MEDIA_PLAY_PAUSE
+key 165 MEDIA_PREVIOUS
+
+key 158 BACK
+key 172 HOME
+
+key 217 SEARCH
+key 580 APP_SWITCH
+key 582 ASSIST
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+led 0x00 CONTROLLER_1
+led 0x01 CONTROLLER_2
+led 0x02 CONTROLLER_3
+led 0x03 CONTROLLER_4
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c216.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c216.kl
new file mode 100644
index 0000000000..8bc142f0ca
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c216.kl
@@ -0,0 +1,37 @@
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Logitech Dual Action Controller
+#
+
+key 0x121 BUTTON_A
+key 0x122 BUTTON_B
+key 0x120 BUTTON_X
+key 0x123 BUTTON_Y
+key 0x124 BUTTON_L1
+key 0x125 BUTTON_R1
+key 0x126 BUTTON_L2
+key 0x127 BUTTON_R2
+key 0x128 BACK
+key 0x129 BUTTON_START
+key 0x12a BUTTON_THUMBL
+key 0x12b BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c219.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c219.kl
new file mode 100644
index 0000000000..2fa964ce53
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c219.kl
@@ -0,0 +1,35 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Logitech Logitech Cordless RumblePad 2
+
+key 305 BUTTON_A
+key 306 BUTTON_B
+key 304 BUTTON_X
+key 307 BUTTON_Y
+key 308 BUTTON_L1
+key 309 BUTTON_R1
+key 310 BUTTON_L2
+key 311 BUTTON_R2
+key 313 BUTTON_START
+key 312 BACK
+key 314 BUTTON_THUMBL
+key 315 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21d.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21d.kl
new file mode 100644
index 0000000000..3fbdecc174
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21d.kl
@@ -0,0 +1,36 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Logitech F310
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 316 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 GAS
+axis 0x02 BRAKE
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21e.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21e.kl
new file mode 100644
index 0000000000..998074331d
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21e.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Logitech Gamepad F510
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21f.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21f.kl
new file mode 100644
index 0000000000..a9ba3781ec
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c21f.kl
@@ -0,0 +1,36 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Logitech Wireless Gamepad F710
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 316 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c242.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c242.kl
new file mode 100644
index 0000000000..51eb44a621
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c242.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Logitech Chillstream Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c294.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c294.kl
new file mode 100644
index 0000000000..5492f49758
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c294.kl
@@ -0,0 +1,53 @@
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Logitech G25 Racing Wheel (in Compatibility Mode)
+#
+
+# 4 way buttons above hat
+key 0x121 BUTTON_A
+key 0x123 BUTTON_B
+key 0x120 BUTTON_X
+key 0x122 BUTTON_Y
+
+# Row of buttons under hat
+key 0x12b BUTTON_1
+key 0x128 BUTTON_2
+key 0x129 BUTTON_3
+key 0x12a BUTTON_4
+
+# Gear shift positions
+# 0x12a top-left gear (aliased as BUTTON_4)
+# 0x12b bottom-left gear (aliased as BUTTON_1)
+
+# Buttons on wheel
+key 0x127 BUTTON_L1
+key 0x126 BUTTON_R1
+
+# Toggles under wheel
+key 0x125 BUTTON_L2
+key 0x124 BUTTON_R2
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Steering Wheel
+axis 0x00 WHEEL
+
+# Accelerator / Brake
+# 00..7e : accelerator
+# 80..ff : brake
+axis 0x01 split 0x7f GAS BRAKE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c299.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c299.kl
new file mode 100644
index 0000000000..d42963dbc9
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c299.kl
@@ -0,0 +1,62 @@
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Logitech G25 Racing Wheel (in Native Mode)
+#
+
+# 4 way buttons above hat
+key 0x121 BUTTON_A
+key 0x123 BUTTON_B
+key 0x120 BUTTON_X
+key 0x122 BUTTON_Y
+
+# Row of buttons under hat
+key 0x12b BUTTON_1
+key 0x128 BUTTON_2
+key 0x129 BUTTON_3
+key 0x12a BUTTON_4
+
+# Gear shift positions
+key 0x12c BUTTON_5
+key 0x12d BUTTON_6
+key 0x12e BUTTON_7
+key 0x12f BUTTON_8
+key 0x2d0 BUTTON_9
+key 0x2d1 BUTTON_10
+key 0x2d2 BUTTON_11
+
+# Buttons on wheel
+key 0x127 BUTTON_L1
+key 0x126 BUTTON_R1
+
+# Toggles under wheel
+key 0x125 BUTTON_L2
+key 0x124 BUTTON_R2
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Steering Wheel
+axis 0x00 WHEEL
+
+# Clutch
+axis 0x01 invert GENERIC_1
+
+# Accelerator
+axis 0x02 invert GAS
+
+# Brake
+axis 0x05 invert BRAKE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c532.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c532.kl
new file mode 100644
index 0000000000..741c2e18b2
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_046d_Product_c532.kl
@@ -0,0 +1,133 @@
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Logitech Revue Wireless keyboard
+#
+# Notes:
+# - The GRAVE key is emulated by the keyboard.
+# ALT + LEFT_BRACKET produces GRAVE.
+# ALT + RIGHT_BRACKET produces SHIFT + GRAVE.
+# - FORWARD_DEL is produced by fn + BACKSPACE
+# - PAGE_UP / PAGE_DOWN is produced by fn + CHANNEL_UP / CHANNEL_DOWN
+# - The AVR / STB / TV power and input buttons seem to be non-functional
+# as well as several of the other fn buttons and the PIP button?
+
+key 1 ESCAPE
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+key 12 MINUS
+key 13 EQUALS
+key 14 DEL
+key 15 TAB
+key 16 Q
+key 17 W
+key 18 E
+key 19 R
+key 20 T
+key 21 Y
+key 22 U
+key 23 I
+key 24 O
+key 25 P
+key 26 LEFT_BRACKET
+key 27 RIGHT_BRACKET
+key 28 ENTER
+key 29 CTRL_LEFT
+key 30 A
+key 31 S
+key 32 D
+key 33 F
+key 34 G
+key 35 H
+key 36 J
+key 37 K
+key 38 L
+key 39 SEMICOLON
+key 40 APOSTROPHE
+key 41 GRAVE
+key 42 SHIFT_LEFT
+key 43 BACKSLASH
+key 44 Z
+key 45 X
+key 46 C
+key 47 V
+key 48 B
+key 49 N
+key 50 M
+key 51 COMMA
+key 52 PERIOD
+key 53 SLASH
+key 54 SHIFT_RIGHT
+key 56 ALT_RIGHT
+key 57 SPACE
+key 58 CAPS_LOCK
+key 59 F1
+key 60 F2
+key 61 F3
+key 62 F4
+key 63 F5
+key 64 F6
+key 65 F7
+key 66 F8
+key 67 F9
+key 68 F10
+key 87 F11
+key 88 F12
+key 96 DPAD_CENTER
+key 97 CTRL_RIGHT
+key 102 MOVE_HOME
+key 103 DPAD_UP
+key 104 PAGE_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 107 MOVE_END
+key 108 DPAD_DOWN
+key 109 PAGE_DOWN
+key 110 NUMPAD_ENTER
+key 111 FORWARD_DEL
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+key 119 MEDIA_PAUSE
+key 125 SEARCH
+key 127 MENU
+key 156 BOOKMARK
+key 158 BACK
+key 163 MEDIA_NEXT
+key 165 MEDIA_PREVIOUS
+key 166 MEDIA_STOP
+key 167 MEDIA_RECORD
+key 168 MEDIA_REWIND
+key 172 HOME
+key 207 MEDIA_PLAY
+key 208 MEDIA_FAST_FORWARD
+# key 288 left mouse button
+# key 289 right mouse button (fn + button)
+key 362 GUIDE
+key 366 DVR
+key 377 TV
+key 402 CHANNEL_UP
+key 403 CHANNEL_DOWN
+key 418 ZOOM_IN
+key 419 ZOOM_OUT
+
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268.kl
new file mode 100644
index 0000000000..08d1c34f9e
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268.kl
@@ -0,0 +1,87 @@
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R)3 Controller
+#
+
+key 0x124 DPAD_UP
+key 0x125 DPAD_RIGHT
+key 0x126 DPAD_DOWN
+key 0x127 DPAD_LEFT
+
+key 0x12e BUTTON_A
+key 0x12d BUTTON_B
+key 0x12f BUTTON_X
+key 0x12c BUTTON_Y
+key 0x12a BUTTON_L1
+key 0x12b BUTTON_R1
+key 0x128 BUTTON_L2
+key 0x129 BUTTON_R2
+key 0x121 BUTTON_THUMBL
+key 0x122 BUTTON_THUMBR
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# DPAD
+# axis 0x2c -HAT_Y
+# axis 0x2d +HAT_X
+# axis 0x2e +HAT_Y
+# axis 0x2f -HAT_X
+
+# L2 trigger
+axis 0x30 LTRIGGER
+
+# R2 trigger
+axis 0x31 RTRIGGER
+
+# L1 trigger
+# axis 0x32
+
+# R1 trigger
+# axis 0x33
+
+# Triangle
+# axis 0x34
+
+# Circle
+# axis 0x35
+
+# Cross
+# axis 0x36
+
+# Square
+# axis 0x37
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Select
+key 0x120 BUTTON_SELECT
+# Start
+key 0x123 BUTTON_START
+# PS key
+key 0x2d0 BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8000.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8000.kl
new file mode 100644
index 0000000000..d281b4bb53
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8000.kl
@@ -0,0 +1,65 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R)3 Controller
+# - Version 0x8000 and 0x8100 are for Linux hid-sony driver >=4.12
+# and when connected over Bluetooth
+#
+
+key 0x220 DPAD_UP
+key 0x223 DPAD_RIGHT
+key 0x221 DPAD_DOWN
+key 0x222 DPAD_LEFT
+
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x134 BUTTON_X
+key 0x133 BUTTON_Y
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+key 0x13d BUTTON_THUMBL
+key 0x13e BUTTON_THUMBR
+
+# left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# L2 trigger
+axis 0x02 LTRIGGER
+
+# R2 trigger
+axis 0x05 RTRIGGER
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Select
+key 0x13a BUTTON_SELECT
+# Start
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8100.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8100.kl
new file mode 100644
index 0000000000..d281b4bb53
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8100.kl
@@ -0,0 +1,65 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R)3 Controller
+# - Version 0x8000 and 0x8100 are for Linux hid-sony driver >=4.12
+# and when connected over Bluetooth
+#
+
+key 0x220 DPAD_UP
+key 0x223 DPAD_RIGHT
+key 0x221 DPAD_DOWN
+key 0x222 DPAD_LEFT
+
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x134 BUTTON_X
+key 0x133 BUTTON_Y
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+key 0x13d BUTTON_THUMBL
+key 0x13e BUTTON_THUMBR
+
+# left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# L2 trigger
+axis 0x02 LTRIGGER
+
+# R2 trigger
+axis 0x05 RTRIGGER
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Select
+key 0x13a BUTTON_SELECT
+# Start
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8111.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8111.kl
new file mode 100644
index 0000000000..3eafea0f6c
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0268_Version_8111.kl
@@ -0,0 +1,65 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R)3 Controller
+# - Version 0x8111 is for Linux hid-sony driver >=4.12 and when
+# connected over USB
+#
+
+key 0x220 DPAD_UP
+key 0x223 DPAD_RIGHT
+key 0x221 DPAD_DOWN
+key 0x222 DPAD_LEFT
+
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x134 BUTTON_X
+key 0x133 BUTTON_Y
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+key 0x13d BUTTON_THUMBL
+key 0x13e BUTTON_THUMBR
+
+# left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# L2 trigger
+axis 0x02 LTRIGGER
+
+# R2 trigger
+axis 0x05 RTRIGGER
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Select
+key 0x13a BUTTON_SELECT
+# Start
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4.kl
new file mode 100644
index 0000000000..c8b4fc363f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4.kl
@@ -0,0 +1,78 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 Controller
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x130 BUTTON_X
+# Cross
+key 0x131 BUTTON_A
+# Circle
+key 0x132 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# L2 axis
+axis 0x03 LTRIGGER
+# R2 axis
+axis 0x04 RTRIGGER
+
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 0x13a BUTTON_THUMBL
+# Right stick click
+key 0x13b BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x138 BUTTON_SELECT
+# Options
+key 0x139 BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# Touchpad press
+# The touchpad for this joystick will become a separate input device in future releases
+# and this button will be equivalent to left mouse button
+# Therefore, map it to KEYCODE_BUTTON_1 here to allow apps to still handle this on earlier versions
+key 0x13d BUTTON_1
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8000.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8000.kl
new file mode 100644
index 0000000000..a877c4cc75
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8000.kl
@@ -0,0 +1,76 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 Controller
+# - Version 0x8000 and 0x8100 are for Linux hid-sony driver >=4.10
+# and when connected over Bluetooth
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x13a BUTTON_SELECT
+# Options
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# In kernel versions >= 4.10, the touchpad is a separate input device,
+# so the touchpad button click will not be covered by this layout.
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8100.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8100.kl
new file mode 100644
index 0000000000..a877c4cc75
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8100.kl
@@ -0,0 +1,76 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 Controller
+# - Version 0x8000 and 0x8100 are for Linux hid-sony driver >=4.10
+# and when connected over Bluetooth
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x13a BUTTON_SELECT
+# Options
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# In kernel versions >= 4.10, the touchpad is a separate input device,
+# so the touchpad button click will not be covered by this layout.
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8111.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8111.kl
new file mode 100644
index 0000000000..1473c4e535
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_05c4_Version_8111.kl
@@ -0,0 +1,76 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 Controller
+# - Version 0x8111 is for Linux hid-sony driver >=4.10 and when
+# connected over USB
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x13a BUTTON_SELECT
+# Options
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# In kernel versions >= 4.10, the touchpad is a separate input device,
+# so the touchpad button click will not be covered by this layout.
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc.kl
new file mode 100644
index 0000000000..c8b4fc363f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc.kl
@@ -0,0 +1,78 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 Controller
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x130 BUTTON_X
+# Cross
+key 0x131 BUTTON_A
+# Circle
+key 0x132 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# L2 axis
+axis 0x03 LTRIGGER
+# R2 axis
+axis 0x04 RTRIGGER
+
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 0x13a BUTTON_THUMBL
+# Right stick click
+key 0x13b BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x138 BUTTON_SELECT
+# Options
+key 0x139 BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# Touchpad press
+# The touchpad for this joystick will become a separate input device in future releases
+# and this button will be equivalent to left mouse button
+# Therefore, map it to KEYCODE_BUTTON_1 here to allow apps to still handle this on earlier versions
+key 0x13d BUTTON_1
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8000.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8000.kl
new file mode 100644
index 0000000000..a877c4cc75
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8000.kl
@@ -0,0 +1,76 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 Controller
+# - Version 0x8000 and 0x8100 are for Linux hid-sony driver >=4.10
+# and when connected over Bluetooth
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x13a BUTTON_SELECT
+# Options
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# In kernel versions >= 4.10, the touchpad is a separate input device,
+# so the touchpad button click will not be covered by this layout.
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8100.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8100.kl
new file mode 100644
index 0000000000..a877c4cc75
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8100.kl
@@ -0,0 +1,76 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 Controller
+# - Version 0x8000 and 0x8100 are for Linux hid-sony driver >=4.10
+# and when connected over Bluetooth
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x13a BUTTON_SELECT
+# Options
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# In kernel versions >= 4.10, the touchpad is a separate input device,
+# so the touchpad button click will not be covered by this layout.
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8111.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8111.kl
new file mode 100644
index 0000000000..1473c4e535
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_09cc_Version_8111.kl
@@ -0,0 +1,76 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 Controller
+# - Version 0x8111 is for Linux hid-sony driver >=4.10 and when
+# connected over USB
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x13a BUTTON_SELECT
+# Options
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# In kernel versions >= 4.10, the touchpad is a separate input device,
+# so the touchpad button click will not be covered by this layout.
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ba0.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ba0.kl
new file mode 100644
index 0000000000..bc6fc3b5e4
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ba0.kl
@@ -0,0 +1,70 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 USB Dongle
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x130 BUTTON_X
+# Cross
+key 0x131 BUTTON_A
+# Circle
+key 0x132 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# L2 axis
+axis 0x03 LTRIGGER
+# R2 axis
+axis 0x04 RTRIGGER
+
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 0x13a BUTTON_THUMBL
+# Right stick click
+key 0x13b BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x138 BUTTON_SELECT
+# Options
+key 0x139 BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# Touchpad press
+# The touchpad for this joystick will become a separate input device in future releases
+# and this button will be equivalent to left mouse button
+# Therefore, map it to KEYCODE_BUTTON_1 here to allow apps to still handle this on earlier versions
+key 0x13d BUTTON_1
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ba0_Version_8111.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ba0_Version_8111.kl
new file mode 100644
index 0000000000..8b85a38b43
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ba0_Version_8111.kl
@@ -0,0 +1,67 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualShock 4 USB Dongle
+# - Version 0x8111 is for Linux hid-sony driver >=4.10
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x13a BUTTON_SELECT
+# Options
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# In kernel versions >= 4.10, the touchpad is a separate input device,
+# so the touchpad button click will not be covered by this layout.
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ce6.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ce6.kl
new file mode 100644
index 0000000000..411dd95219
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ce6.kl
@@ -0,0 +1,73 @@
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualSense Controller
+#
+
+# Only use this key layout if we have HID_PLAYSTATION!
+requires_kernel_config CONFIG_HID_PLAYSTATION
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x13a BUTTON_SELECT
+# Options
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ce6_fallback.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ce6_fallback.kl
new file mode 100644
index 0000000000..d1a364ce8c
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0ce6_fallback.kl
@@ -0,0 +1,75 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualSense Controller
+#
+
+# Use this if HID_PLAYSTATION is not available
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 304 BUTTON_X
+# Cross
+key 305 BUTTON_A
+# Circle
+key 306 BUTTON_B
+# Triangle
+key 307 BUTTON_Y
+
+key 308 BUTTON_L1
+key 309 BUTTON_R1
+key 310 BUTTON_L2
+key 311 BUTTON_R2
+
+# L2 axis
+axis 0x03 LTRIGGER
+# R2 axis
+axis 0x04 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 314 BUTTON_THUMBL
+# Right stick click
+key 315 BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 312 BUTTON_SELECT
+# Options / three horizontal lines
+key 313 BUTTON_START
+# PS key
+key 316 BUTTON_MODE
+
+# Touchpad press
+key 317 BUTTON_1
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0df2.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0df2.kl
new file mode 100644
index 0000000000..a47b310328
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0df2.kl
@@ -0,0 +1,73 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualSense Edge Controller
+#
+
+# Only use this key layout if we have HID_PLAYSTATION!
+requires_kernel_config CONFIG_HID_PLAYSTATION
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 0x13a BUTTON_SELECT
+# Options / three horizontal lines
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0df2_fallback.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0df2_fallback.kl
new file mode 100644
index 0000000000..bfebb179a3
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_054c_Product_0df2_fallback.kl
@@ -0,0 +1,75 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Sony Playstation(R) DualSense Edge Controller
+#
+
+# Use this if HID_PLAYSTATION is not available
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 304 BUTTON_X
+# Cross
+key 305 BUTTON_A
+# Circle
+key 306 BUTTON_B
+# Triangle
+key 307 BUTTON_Y
+
+key 308 BUTTON_L1
+key 309 BUTTON_R1
+key 310 BUTTON_L2
+key 311 BUTTON_R2
+
+# L2 axis
+axis 0x03 LTRIGGER
+# R2 axis
+axis 0x04 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 314 BUTTON_THUMBL
+# Right stick click
+key 315 BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 312 BUTTON_SELECT
+# Options / three horizontal lines
+key 313 BUTTON_START
+# PS key
+key 316 BUTTON_MODE
+
+# Touchpad press
+key 317 BUTTON_1
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2004.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2004.kl
new file mode 100644
index 0000000000..9eaa36d740
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2004.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Elecom JC-U3613M
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2010.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2010.kl
new file mode 100644
index 0000000000..09e15eaa62
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2010.kl
@@ -0,0 +1,48 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Elecom JC-U4113S in DirectInput Mode (D mode).
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 306 BUTTON_A
+key 307 BUTTON_B
+key 304 BUTTON_X
+key 305 BUTTON_Y
+
+key 308 BUTTON_L1
+key 309 BUTTON_R1
+key 310 BUTTON_L2
+key 311 BUTTON_R2
+
+key 312 BUTTON_THUMBL
+key 313 BUTTON_THUMBR
+
+key 314 BACK
+key 315 BUTTON_START
+
+# Left and right stick.
+axis 0x00 X
+axis 0x01 Y
+axis 0x05 Z
+axis 0x02 RZ
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# "Guide" button (Xbox key).
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2013.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2013.kl
new file mode 100644
index 0000000000..c2a74a9fd4
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_056e_Product_2013.kl
@@ -0,0 +1,44 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Elecom JC-U4113S in XInput Mode (X mode).
+#
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Left and right stick.
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+
+axis 0x02 BRAKE
+axis 0x05 GAS
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# "Guide" button (Xbox key).
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_057e_Product_2009.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_057e_Product_2009.kl
new file mode 100644
index 0000000000..7491ee562b
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_057e_Product_2009.kl
@@ -0,0 +1,84 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Nintendo Switch Pro Controller - HAC-013 - Bluetooth
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Button labeled as "B" but should really produce keycode "A"
+key 0x130 BUTTON_A
+# Button labeled as "A" but should really produce keycode "B"
+key 0x131 BUTTON_B
+# Button labeled as "X" but should really product keycode "Y"
+key 0x133 BUTTON_Y
+# Button labeled as "Y" but should really produce keycode "X"
+key 0x134 BUTTON_X
+
+# Button labeled as "L"
+key 0x136 BUTTON_L1
+# Button labeled as "R"
+key 0x137 BUTTON_R1
+
+# No LT / RT axes on this controller. Instead, there are keys.
+# Trigger labeled as "ZL"
+key 0x138 BUTTON_L2
+# Trigger labeled as "ZR"
+key 0x139 BUTTON_R2
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click (generates linux BTN_SELECT)
+key 0x13d BUTTON_THUMBL
+# Right stick click (generates linux BTN_START)
+key 0x13e BUTTON_THUMBR
+
+# Currently, the dpad produces key events
+key 0x220 DPAD_UP
+key 0x221 DPAD_DOWN
+key 0x222 DPAD_LEFT
+key 0x223 DPAD_RIGHT
+
+# Hat - currently not being produced by hid-nintendo, but an upcoming patch set will change the behaviour.
+# Keep these mappings in anticipation of that change
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Minus
+key 0x13a BUTTON_SELECT
+
+# Plus
+key 0x13b BUTTON_START
+
+# Circle
+key 0x135 BUTTON_MODE
+
+# Home key
+key 0x13c HOME
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0583_Product_2060.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0583_Product_2060.kl
new file mode 100644
index 0000000000..92c8a14a2f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0583_Product_2060.kl
@@ -0,0 +1,27 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# ION GO PAD
+
+key 288 BUTTON_A
+key 289 BUTTON_B
+key 290 BUTTON_X
+key 291 BUTTON_Y
+key 294 BUTTON_L1
+key 295 BUTTON_R1
+key 292 BUTTON_L2
+key 293 BUTTON_R2
+
+axis 0x00 HAT_X
+axis 0x01 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_05ac_Product_0239.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_05ac_Product_0239.kl
new file mode 100644
index 0000000000..b0c358e7d8
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_05ac_Product_0239.kl
@@ -0,0 +1,119 @@
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Apple Wireless Keyboard
+#
+# Notes:
+# - Keys such as PAGE_UP and FORWARD_DEL are produced using the
+# function key.
+# - Special function keys for brightness control, etc. are not
+# implemented here.
+
+key 1 ESCAPE
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+key 12 MINUS
+key 13 EQUALS
+key 14 DEL
+key 15 TAB
+key 16 Q
+key 17 W
+key 18 E
+key 19 R
+key 20 T
+key 21 Y
+key 22 U
+key 23 I
+key 24 O
+key 25 P
+key 26 LEFT_BRACKET
+key 27 RIGHT_BRACKET
+key 28 ENTER
+key 29 CTRL_LEFT
+key 30 A
+key 31 S
+key 32 D
+key 33 F
+key 34 G
+key 35 H
+key 36 J
+key 37 K
+key 38 L
+key 39 SEMICOLON
+key 40 APOSTROPHE
+key 41 GRAVE
+key 42 SHIFT_LEFT
+key 43 BACKSLASH
+key 44 Z
+key 45 X
+key 46 C
+key 47 V
+key 48 B
+key 49 N
+key 50 M
+key 51 COMMA
+key 52 PERIOD
+key 53 SLASH
+key 54 SHIFT_RIGHT
+key 56 ALT_LEFT
+key 57 SPACE
+key 58 CAPS_LOCK
+key 59 F1
+key 60 F2
+key 61 F3
+key 62 F4
+key 63 F5
+key 64 F6
+key 65 F7
+key 66 F8
+key 67 F9
+key 68 F10
+key 87 F11
+key 88 F12
+key 100 ALT_RIGHT
+key 102 MOVE_HOME
+key 103 DPAD_UP
+key 104 PAGE_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 107 MOVE_END
+key 108 DPAD_DOWN
+key 109 PAGE_DOWN
+key 110 NUMPAD_ENTER
+key 111 FORWARD_DEL
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+key 120 APP_SWITCH
+key 125 META_LEFT
+key 126 META_RIGHT
+key 161 MEDIA_EJECT
+key 163 MEDIA_NEXT
+key 164 MEDIA_PLAY_PAUSE
+key 165 MEDIA_PREVIOUS
+# key 204 show gadgets
+key 224 BRIGHTNESS_DOWN
+key 225 BRIGHTNESS_UP
+# key 229 blank special function on F5
+# key 230 blank special function on F6
+key 464 FUNCTION
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_06a3_Product_f51a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_06a3_Product_f51a.kl
new file mode 100644
index 0000000000..e52f25724f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_06a3_Product_f51a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Saitek P3600
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4716.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4716.kl
new file mode 100644
index 0000000000..5f3d4aa4b2
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4716.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz Wired Xbox 360 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4718.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4718.kl
new file mode 100644
index 0000000000..756e1e75fb
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4718.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz Street Fighter IV FightStick SE
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4726.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4726.kl
new file mode 100644
index 0000000000..9d8deb36e4
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4726.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz Xbox 360 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4736.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4736.kl
new file mode 100644
index 0000000000..c556e25fde
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4736.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz MicroCon Gamepad
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4740.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4740.kl
new file mode 100644
index 0000000000..cdb72683b5
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_4740.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz Beat Pad
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_9871.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_9871.kl
new file mode 100644
index 0000000000..f404065236
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_9871.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz Portable Drum
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_b726.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_b726.kl
new file mode 100644
index 0000000000..05b737f8f9
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_b726.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz Xbox controller - MW2
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_beef.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_beef.kl
new file mode 100644
index 0000000000..f969e73ec5
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_beef.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz JOYTECH NEO SE Advanced GamePad
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb02.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb02.kl
new file mode 100644
index 0000000000..bc2fc35ae1
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb02.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Saitek Cyborg Rumble Pad - PC/Xbox 360
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb03.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb03.kl
new file mode 100644
index 0000000000..dcbf6b7487
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb03.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Saitek P3200 Rumble Pad - PC/Xbox 360
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb29.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb29.kl
new file mode 100644
index 0000000000..fe81d1c217
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_cb29.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Saitek Aviator Stick AV8R02
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_f738.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_f738.kl
new file mode 100644
index 0000000000..2c99380756
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0738_Product_f738.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Super SFIV FightStick TE S
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_07ff_Product_ffff.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_07ff_Product_ffff.kl
new file mode 100644
index 0000000000..637c01bddd
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_07ff_Product_ffff.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz GamePad
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0957_Product_0001.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0957_Product_0001.kl
new file mode 100644
index 0000000000..0241f36e9a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0957_Product_0001.kl
@@ -0,0 +1,83 @@
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Key Layout file for Google Reference RCU Remote.
+#
+
+key 116 POWER WAKE
+key 217 ASSIST WAKE
+
+key 103 DPAD_UP
+key 108 DPAD_DOWN
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 353 DPAD_CENTER
+
+key 158 BACK
+key 172 HOME WAKE
+
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+
+key usage 0x00070037 PERIOD
+
+# custom keys
+key usage 0x000c01BB TV_INPUT
+
+key usage 0x000c0185 TV_TELETEXT
+key usage 0x000c0061 CAPTIONS
+
+key usage 0x000c01BD INFO
+
+key usage 0x000c0069 PROG_RED
+key usage 0x000c006A PROG_GREEN
+key usage 0x000c006C PROG_YELLOW
+key usage 0x000c006B PROG_BLUE
+key usage 0x000c00B4 MEDIA_SKIP_BACKWARD
+key usage 0x000c00CD MEDIA_PLAY_PAUSE
+key usage 0x000c00B2 MEDIA_RECORD
+key usage 0x000c00B3 MEDIA_SKIP_FORWARD
+
+key usage 0x000c022A BOOKMARK
+key usage 0x000c01A2 ALL_APPS
+key usage 0x000c019C PROFILE_SWITCH
+
+key usage 0x000c0096 SETTINGS
+key usage 0x000c009F NOTIFICATION
+
+key usage 0x000c008D GUIDE
+key usage 0x000c0089 TV
+
+key usage 0x000c0187 FEATURED_APP_1 WAKE #FreeTv
+
+key usage 0x000c009C CHANNEL_UP
+key usage 0x000c009D CHANNEL_DOWN
+
+key usage 0x000c0077 BUTTON_3 WAKE #YouTube
+key usage 0x000c0078 BUTTON_4 WAKE #Netflix
+key usage 0x000c0079 BUTTON_6 WAKE
+key usage 0x000c007A BUTTON_7 WAKE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0957_Product_0031.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0957_Product_0031.kl
new file mode 100644
index 0000000000..dd9fbe54c4
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0957_Product_0031.kl
@@ -0,0 +1,82 @@
+# Copyright 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Key Layout file for Google Reference RCU Remote with customizable button.
+#
+
+key 116 POWER WAKE
+key 217 ASSIST WAKE
+key 423 MACRO_1 WAKE
+
+key 103 DPAD_UP
+key 108 DPAD_DOWN
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 353 DPAD_CENTER
+
+key 158 BACK
+key 172 HOME WAKE
+
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+
+# custom keys
+key usage 0x000c01BB TV_INPUT
+
+key usage 0x000c0185 TV_TELETEXT
+key usage 0x000c0061 CAPTIONS
+
+key usage 0x000c01BD INFO
+key usage 0x000c0037 PERIOD
+
+key usage 0x000c0069 PROG_RED
+key usage 0x000c006A PROG_GREEN
+key usage 0x000c006C PROG_YELLOW
+key usage 0x000c006B PROG_BLUE
+key usage 0x000c00B4 MEDIA_SKIP_BACKWARD
+key usage 0x000c00CD MEDIA_PLAY_PAUSE
+key usage 0x000c00B2 MEDIA_RECORD
+key usage 0x000c00B3 MEDIA_SKIP_FORWARD
+
+key usage 0x000c022A BOOKMARK
+key usage 0x000c01A2 ALL_APPS
+key usage 0x000c019C PROFILE_SWITCH
+
+key usage 0x000c0096 SETTINGS
+key usage 0x000c009F NOTIFICATION
+
+key usage 0x000c008D GUIDE
+key usage 0x000c0089 TV
+
+key usage 0x000c0187 FEATURED_APP_1 WAKE #FreeTv
+
+key usage 0x000c009C CHANNEL_UP
+key usage 0x000c009D CHANNEL_DOWN
+
+key usage 0x000c0077 BUTTON_3 WAKE #YouTube
+key usage 0x000c0078 BUTTON_4 WAKE #Netflix
+key usage 0x000c0079 BUTTON_6 WAKE
+key usage 0x000c007A BUTTON_7 WAKE
\ No newline at end of file
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0b05_Product_4500.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0b05_Product_4500.kl
new file mode 100644
index 0000000000..a7d519e903
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0b05_Product_4500.kl
@@ -0,0 +1,42 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Asus Gamepad
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 316 BUTTON_MODE
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+key 158 BACK
+key 172 HOME
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+led 0x00 CONTROLLER_1
+led 0x01 CONTROLLER_2
+led 0x02 CONTROLLER_3
+led 0x03 CONTROLLER_4
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0113.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0113.kl
new file mode 100644
index 0000000000..90e1f75d19
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0113.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Afterglow AX.1 Gamepad for Xbox 360
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_011f.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_011f.kl
new file mode 100644
index 0000000000..8c63c6bdf0
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_011f.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Rock Candy Gamepad Wired Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0131.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0131.kl
new file mode 100644
index 0000000000..368c37606d
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0131.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP EA Sports Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0133.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0133.kl
new file mode 100644
index 0000000000..815902edb8
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0133.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Xbox 360 Wired Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0139.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0139.kl
new file mode 100644
index 0000000000..8e2ae13f90
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0139.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Afterglow Prismatic Wired Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_013a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_013a.kl
new file mode 100644
index 0000000000..3f81983bdc
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_013a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Xbox One Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0146.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0146.kl
new file mode 100644
index 0000000000..6ddd056c15
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0146.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Rock Candy Wired Controller for Xbox One
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0147.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0147.kl
new file mode 100644
index 0000000000..6745b7c5c2
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0147.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Marvel Xbox One Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0161.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0161.kl
new file mode 100644
index 0000000000..3f81983bdc
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0161.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Xbox One Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0162.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0162.kl
new file mode 100644
index 0000000000..3f81983bdc
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0162.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Xbox One Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0163.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0163.kl
new file mode 100644
index 0000000000..3f81983bdc
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0163.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Xbox One Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0164.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0164.kl
new file mode 100644
index 0000000000..0fdfd32922
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0164.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Battlefield One
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0165.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0165.kl
new file mode 100644
index 0000000000..f9731e025f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0165.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Titanfall 2
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0201.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0201.kl
new file mode 100644
index 0000000000..5b4c167d99
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0201.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Pelican PL-3601 'TSZ' Wired Xbox 360 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0213.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0213.kl
new file mode 100644
index 0000000000..9317346faa
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0213.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Afterglow Gamepad for Xbox 360
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_021f.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_021f.kl
new file mode 100644
index 0000000000..f8d3f0c080
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_021f.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Rock Candy Gamepad for Xbox 360
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0246.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0246.kl
new file mode 100644
index 0000000000..daf8e4525a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0246.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Rock Candy Gamepad for Xbox One 2015
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02a4.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02a4.kl
new file mode 100644
index 0000000000..9ffae33bc3
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02a4.kl
@@ -0,0 +1,54 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Wired Controller for Xbox One - Stealth Series
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02a6.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02a6.kl
new file mode 100644
index 0000000000..99a59317e7
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02a6.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Wired Controller for Xbox One - Camo Series
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02ab.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02ab.kl
new file mode 100644
index 0000000000..071a56c8c0
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_02ab.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Controller for Xbox One
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0301.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0301.kl
new file mode 100644
index 0000000000..a3b982d514
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0301.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Logic3 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0346.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0346.kl
new file mode 100644
index 0000000000..6fefbf7ca2
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0346.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Rock Candy Gamepad for Xbox One 2016
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0401.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0401.kl
new file mode 100644
index 0000000000..a3b982d514
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0401.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Logic3 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0413.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0413.kl
new file mode 100644
index 0000000000..90e1f75d19
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0413.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Afterglow AX.1 Gamepad for Xbox 360
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0501.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0501.kl
new file mode 100644
index 0000000000..35831d1486
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_0501.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Xbox 360 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_f501.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_f501.kl
new file mode 100644
index 0000000000..b46c005353
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_f501.kl
@@ -0,0 +1,55 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# XBox-compatible USB Controller
+#
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+
+# Button labeled as "BACK" (left-pointing triangle)
+key 314 BUTTON_SELECT
+
+# The branded "X" button in the center of the controller
+key 316 BUTTON_MODE
+
+# Button labeled as "START" (right-pointing triangle)
+key 315 BUTTON_START
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_f900.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_f900.kl
new file mode 100644
index 0000000000..44848ba0cf
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0e6f_Product_f900.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Afterglow AX.1
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_000a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_000a.kl
new file mode 100644
index 0000000000..b3aea049f6
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_000a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Hori Co. DOA4 FightStick
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_000c.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_000c.kl
new file mode 100644
index 0000000000..49c3addd78
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_000c.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Hori PadEX Turbo
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_0067.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_0067.kl
new file mode 100644
index 0000000000..0dfccebd73
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_0067.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# HORIPAD ONE
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_00c1.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_00c1.kl
new file mode 100644
index 0000000000..c74512a3a4
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_0f0d_Product_00c1.kl
@@ -0,0 +1,55 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Device name: HORI CO.,LTD. HORIPAD S
+# HORIPAD for Nintendo Switch, USB game controller
+# https://hori.co.uk/horipad-for-nintendo-switch/
+
+# Button labeled as "Y" but should really produce keycode "X"
+key 0x130 BUTTON_X
+# Button labeled as "B" but should really produce keycode "A"
+key 0x131 BUTTON_A
+# Button labeled as "A" but should really produce keycode "B"
+key 0x132 BUTTON_B
+# Button labeled as "X" but should really product keycode "Y"
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# Minus
+key 0x138 BUTTON_SELECT
+# Plus
+key 0x139 BUTTON_START
+
+# Analog stick buttons
+key 0x13a BUTTON_THUMBL
+key 0x13b BUTTON_THUMBR
+
+# Home
+key 0x13c HOME
+# Capture
+key 0x13d BUTTON_MODE
+
+# Left analog stick
+axis 0x00 X
+axis 0x01 Y
+# Right analog stick
+axis 0x02 Z
+axis 0x05 RZ
+# D-pad
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1412.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1412.kl
new file mode 100644
index 0000000000..551b0bd756
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1412.kl
@@ -0,0 +1,31 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Steelseries Free
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 316 BUTTON_SELECT
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1430.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1430.kl
new file mode 100644
index 0000000000..e635c1d8fa
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1430.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# SteelSeries Stratus Duo
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1431.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1431.kl
new file mode 100644
index 0000000000..e635c1d8fa
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1431.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# SteelSeries Stratus Duo
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1434.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1434.kl
new file mode 100644
index 0000000000..8182edd69a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1038_Product_1434.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# SteelSeries Stratus+
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_11c9_Product_55f0.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_11c9_Product_55f0.kl
new file mode 100644
index 0000000000..dbb4a7e6ec
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_11c9_Product_55f0.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Nacon GC-100XF
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_12ab_Product_0301.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_12ab_Product_0301.kl
new file mode 100644
index 0000000000..36956c1386
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_12ab_Product_0301.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP AFTERGLOW AX.1
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_12bd_Product_d015.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_12bd_Product_d015.kl
new file mode 100644
index 0000000000..557d62f135
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_12bd_Product_d015.kl
@@ -0,0 +1,27 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Hitgaming SNES Retro
+
+key 306 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 304 BUTTON_Y
+key 308 BUTTON_L1
+key 309 BUTTON_R1
+key 313 BUTTON_START
+key 312 BUTTON_SELECT
+
+axis 0x00 HAT_X
+axis 0x01 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1430_Product_4748.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1430_Product_4748.kl
new file mode 100644
index 0000000000..dbe83087ab
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1430_Product_4748.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# RedOctane Guitar Hero X-plorer
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1430_Product_f801.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1430_Product_f801.kl
new file mode 100644
index 0000000000..a8f9146203
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1430_Product_f801.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# RedOctane Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_146b_Product_0601.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_146b_Product_0601.kl
new file mode 100644
index 0000000000..ea2f2211a1
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_146b_Product_0601.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# BigBen Interactive XBOX 360 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0037.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0037.kl
new file mode 100644
index 0000000000..39d8b2e6c1
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0037.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Sabertooth
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0705.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0705.kl
new file mode 100644
index 0000000000..611aaec1c2
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0705.kl
@@ -0,0 +1,64 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Raiju Mobile Controller with wired USB interface.
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x133 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x134 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# L2 axis
+axis 0x09 RTRIGGER
+# R2 axis
+axis 0x0a LTRIGGER
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Select key
+key 0x13a BUTTON_SELECT
+# Start key
+key 0x13b BUTTON_START
+# Home key
+key 0xac BUTTON_MODE
+# Back key
+key 0x9e BACK
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0707.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0707.kl
new file mode 100644
index 0000000000..48c171468b
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0707.kl
@@ -0,0 +1,64 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Raiju Mobile Controller with wireless Bluetooth interface.
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x133 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x134 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# L2 axis
+axis 0x09 RTRIGGER
+# R2 axis
+axis 0x0a LTRIGGER
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Select key
+key 0x13a BUTTON_SELECT
+# Start key
+key 0x13b BUTTON_START
+# Home key
+key 0xac BUTTON_MODE
+# Back key
+key 0x9e BACK
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0709.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0709.kl
new file mode 100644
index 0000000000..20ea2ab750
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0709.kl
@@ -0,0 +1,51 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Junglecat Controller with wireless Bluetooth interface.
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 0x133 BUTTON_X
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x134 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Select key
+key 0x13a BUTTON_SELECT
+# Start key
+key 0x13b BUTTON_START
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0900.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0900.kl
new file mode 100644
index 0000000000..4c6c4dda02
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0900.kl
@@ -0,0 +1,48 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Razer Serval
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+key 158 BACK
+key 172 HOME
+
+# Left arrow to the left of the "power" key
+key 0x13a BUTTON_SELECT
+# Right arrow to the right of the "power" key
+key 0x13b BUTTON_START
+# Power key
+key 0x13c BUTTON_MODE
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+led 0x00 CONTROLLER_1
+led 0x01 CONTROLLER_2
+led 0x02 CONTROLLER_3
+led 0x03 CONTROLLER_4
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0a03.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0a03.kl
new file mode 100644
index 0000000000..75775e9939
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_0a03.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Wildcat
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1004.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1004.kl
new file mode 100644
index 0000000000..bfbfed5e9c
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1004.kl
@@ -0,0 +1,65 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Raiju Ultimate Edition Controller with wired USB interface.
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x130 BUTTON_X
+# Cross
+key 0x131 BUTTON_A
+# Circle
+key 0x132 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# L2 axis
+axis 0x09 RTRIGGER
+# R2 axis
+axis 0x0a LTRIGGER
+
+# Left stick click
+key 0x13a BUTTON_THUMBL
+# Right stick click
+key 0x13b BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x138 BUTTON_SELECT
+# Options
+key 0x139 BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# Touchpad press
+key 0x13d BUTTON_1
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1007.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1007.kl
new file mode 100644
index 0000000000..6f6c286b34
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1007.kl
@@ -0,0 +1,65 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Raiju Tournament Edition Controller with wired USB interface.
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x130 BUTTON_X
+# Cross
+key 0x131 BUTTON_A
+# Circle
+key 0x132 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# L2 axis
+axis 0x09 RTRIGGER
+# R2 axis
+axis 0x0a LTRIGGER
+
+# Left stick click
+key 0x13a BUTTON_THUMBL
+# Right stick click
+key 0x13b BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x138 BUTTON_SELECT
+# Options
+key 0x139 BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# Touchpad press
+key 0x13d BUTTON_1
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1009.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1009.kl
new file mode 100644
index 0000000000..c380d5c335
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_1009.kl
@@ -0,0 +1,65 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Raiju Ultimate Edition Controller with wireless Bluetooth interface.
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x130 BUTTON_X
+# Cross
+key 0x131 BUTTON_A
+# Circle
+key 0x132 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# L2 axis
+axis 0x09 RTRIGGER
+# R2 axis
+axis 0x0a LTRIGGER
+
+# Left stick click
+key 0x13a BUTTON_THUMBL
+# Right stick click
+key 0x13b BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x138 BUTTON_SELECT
+# Options
+key 0x139 BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# Touchpad press
+key 0x13d BUTTON_1
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_100a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_100a.kl
new file mode 100644
index 0000000000..b0e966d519
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1532_Product_100a.kl
@@ -0,0 +1,65 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Raiju Tournament Edition Controller with wireless Bluetooth interface.
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x130 BUTTON_X
+# Cross
+key 0x131 BUTTON_A
+# Circle
+key 0x132 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# L2 axis
+axis 0x09 RTRIGGER
+# R2 axis
+axis 0x0a LTRIGGER
+
+# Left stick click
+key 0x13a BUTTON_THUMBL
+# Right stick click
+key 0x13b BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x138 BUTTON_SELECT
+# Options
+key 0x139 BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# Touchpad press
+key 0x13d BUTTON_1
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f00.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f00.kl
new file mode 100644
index 0000000000..0d641cf9f9
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f00.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Power A Mini Pro Elite
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f0a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f0a.kl
new file mode 100644
index 0000000000..9e98aeec7b
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f0a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Xbox Airflo wired controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f10.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f10.kl
new file mode 100644
index 0000000000..7fb0fea910
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_15e4_Product_3f10.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Batarang Xbox 360 controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_162e_Product_beef.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_162e_Product_beef.kl
new file mode 100644
index 0000000000..e7fab5dba3
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_162e_Product_beef.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Joytech Neo-Se Take2
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fd00.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fd00.kl
new file mode 100644
index 0000000000..8407b13064
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fd00.kl
@@ -0,0 +1,38 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Razer Onza Tournament Edition
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 316 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+key 706 DPAD_UP
+key 705 DPAD_RIGHT
+key 707 DPAD_DOWN
+key 704 DPAD_LEFT
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fd01.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fd01.kl
new file mode 100644
index 0000000000..cacc07526b
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fd01.kl
@@ -0,0 +1,36 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Razer Xbox 360 Gamepad
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 316 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fe00.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fe00.kl
new file mode 100644
index 0000000000..467173f001
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1689_Product_fe00.kl
@@ -0,0 +1,36 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Razer Sabertooth Elite
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 316 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_0200.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_0200.kl
new file mode 100644
index 0000000000..d30bcc60e6
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_0200.kl
@@ -0,0 +1,71 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Keyboard map for the android virtual remote running as a gamepad
+#
+
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x133 BUTTON_X
+key 0x134 BUTTON_Y
+
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+key 0x138 BUTTON_L1
+key 0x139 BUTTON_R1
+
+key 0x13a BUTTON_SELECT
+key 0x13b BUTTON_START
+key 0x13c BUTTON_MODE
+
+key 0x13d BUTTON_THUMBL
+key 0x13e BUTTON_THUMBR
+
+key 103 DPAD_UP
+key 108 DPAD_DOWN
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+
+# Generic usage buttons
+key 0x2c0 BUTTON_1
+key 0x2c1 BUTTON_2
+key 0x2c2 BUTTON_3
+key 0x2c3 BUTTON_4
+key 0x2c4 BUTTON_5
+key 0x2c5 BUTTON_6
+key 0x2c6 BUTTON_7
+key 0x2c7 BUTTON_8
+key 0x2c8 BUTTON_9
+key 0x2c9 BUTTON_10
+key 0x2ca BUTTON_11
+key 0x2cb BUTTON_12
+key 0x2cc BUTTON_13
+key 0x2cd BUTTON_14
+key 0x2ce BUTTON_15
+key 0x2cf BUTTON_16
+
+# assistant buttons
+key 0x246 VOICE_ASSIST
+key 0x247 ASSIST
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_2c40.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_2c40.kl
new file mode 100644
index 0000000000..2b42f871b0
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_2c40.kl
@@ -0,0 +1,49 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Odie
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+led 0x00 CONTROLLER_1
+led 0x01 CONTROLLER_2
+led 0x02 CONTROLLER_3
+led 0x03 CONTROLLER_4
+
+# The next 2 buttons do not follow Linux standard because this behaviour was specified by the UX
+# Left arrow to the immediate left of the power button
+key 158 BACK
+# Circle to the immediate right of the power button
+key 172 HOME
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Power button
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_4f80.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_4f80.kl
new file mode 100644
index 0000000000..f6e4dc4091
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_4f80.kl
@@ -0,0 +1,19 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Android Stylus
+#
+
+key 0x242 STYLUS_BUTTON_TAIL
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_5018.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_5018.kl
new file mode 100644
index 0000000000..e95ccb5c47
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_5018.kl
@@ -0,0 +1,84 @@
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Key layout for Google Pixel C Keyboard
+#
+
+# Row 1
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+key 12 MINUS
+key 14 DEL # Backspace
+
+# Row 2
+key 15 TAB
+key 16 Q
+key 17 W
+key 18 E
+key 19 R
+key 20 T
+key 21 Y
+key 22 U
+key 23 I
+key 24 O
+key 25 P
+key 13 EQUALS
+key 28 ENTER
+
+# Row 3
+key 125 META_LEFT # "Search key"
+key 30 A
+key 31 S
+key 32 D
+key 33 F
+key 34 G
+key 35 H
+key 36 J
+key 37 K
+key 38 L
+key 39 SEMICOLON
+key 40 APOSTROPHE
+
+# Row 4
+key 42 SHIFT_LEFT
+key 44 Z
+key 45 X
+key 46 C
+key 47 V
+key 48 B
+key 49 N
+key 50 M
+key 51 COMMA
+key 52 PERIOD
+key 53 SLASH
+key 54 SHIFT_RIGHT
+
+# Row 5
+key 29 CTRL_LEFT
+key 56 ALT_LEFT
+key 57 SPACE
+key 100 ALT_RIGHT
+key 103 DPAD_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 108 DPAD_DOWN
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_9451.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_9451.kl
new file mode 100644
index 0000000000..2a1f897b5e
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_18d1_Product_9451.kl
@@ -0,0 +1,39 @@
+# Copyright 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Key Layout file for flavor of Google Remote Control.
+#
+
+key 116 POWER WAKE
+key 217 ASSIST
+
+key 103 DPAD_UP
+key 108 DPAD_DOWN
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 353 DPAD_CENTER WAKE
+
+key 158 BACK WAKE
+key 172 HOME WAKE
+
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+
+# custom keys
+key usage 0x000c0186 MACRO_1 WAKE
+
+key usage 0x000c0077 BUTTON_3 WAKE #YouTube
+key usage 0x000c0078 BUTTON_4 WAKE #Netflix
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1949_Product_0401.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1949_Product_0401.kl
new file mode 100644
index 0000000000..ab24bcdeda
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1949_Product_0401.kl
@@ -0,0 +1,27 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Amazon Fire TV remote
+
+key 103 DPAD_UP
+key 108 DPAD_DOWN
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 96 DPAD_CENTER
+key 158 BACK
+key 172 HOME
+key 168 MEDIA_REWIND
+key 208 MEDIA_FAST_FORWARD
+key 164 MEDIA_PLAY_PAUSE
+key 217 ASSIST
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_0002.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_0002.kl
new file mode 100644
index 0000000000..d8eaaba4a9
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_0002.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Harmonix Rock Band Guitar
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f016.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f016.kl
new file mode 100644
index 0000000000..00f8559150
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f016.kl
@@ -0,0 +1,36 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Madcatz Gamepad
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 316 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f021.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f021.kl
new file mode 100644
index 0000000000..9fd688b0c6
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f021.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Cats Ghost Recon FS GamePad
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f023.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f023.kl
new file mode 100644
index 0000000000..175a523b5b
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f023.kl
@@ -0,0 +1,35 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Mad Catz MLG GamePad for Xbox 360
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f025.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f025.kl
new file mode 100644
index 0000000000..03aab446cf
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f025.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Mad Catz Call Of Duty
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f027.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f027.kl
new file mode 100644
index 0000000000..216e3daebb
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f027.kl
@@ -0,0 +1,36 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# MadCatz FPS Pro
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 316 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f028.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f028.kl
new file mode 100644
index 0000000000..51733313d9
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f028.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Street Fighter IV FightPad
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f036.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f036.kl
new file mode 100644
index 0000000000..f27de1c0e4
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f036.kl
@@ -0,0 +1,36 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# MadCatz Generic XBox Controller
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BACK
+key 316 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f038.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f038.kl
new file mode 100644
index 0000000000..79e147d06d
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f038.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Street Fighter IV FightStick TE
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f501.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f501.kl
new file mode 100644
index 0000000000..1282532ab5
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f501.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# HoriPad EX2 Turbo
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f506.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f506.kl
new file mode 100644
index 0000000000..3a9d4620ba
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f506.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Hori Real Arcade Pro.EX Premium VLX
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f900.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f900.kl
new file mode 100644
index 0000000000..9cfceb433a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f900.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Harmonix Xbox 360 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f901.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f901.kl
new file mode 100644
index 0000000000..86d45e58ad
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f901.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Gamestop Xbox 360 Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f903.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f903.kl
new file mode 100644
index 0000000000..f61c050059
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f903.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Tron Xbox 360 controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f904.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f904.kl
new file mode 100644
index 0000000000..3e02a24f9e
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_f904.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PDP Versus Fighting Pad
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fa01.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fa01.kl
new file mode 100644
index 0000000000..517413d2ed
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fa01.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# MadCatz GamePad
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fd00.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fd00.kl
new file mode 100644
index 0000000000..fc6a4f8533
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fd00.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Onza TE
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fd01.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fd01.kl
new file mode 100644
index 0000000000..8882abf0f7
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1bad_Product_fd01.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Onza
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1d79_Product_0009.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1d79_Product_0009.kl
new file mode 100644
index 0000000000..a3d5bbdb3c
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_1d79_Product_0009.kl
@@ -0,0 +1,40 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Nyko Playpad / Playpad Pro
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 158 BACK
+key 172 HOME
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+key 103 DPAD_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 108 DPAD_DOWN
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_22b8_Product_093d.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_22b8_Product_093d.kl
new file mode 100644
index 0000000000..2749c5ba98
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_22b8_Product_093d.kl
@@ -0,0 +1,105 @@
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Motorola Bluetooth Wireless Keyboard.
+#
+
+key 1 BACK
+key 2 1
+key 3 2
+key 4 3
+key 5 4
+key 6 5
+key 7 6
+key 8 7
+key 9 8
+key 10 9
+key 11 0
+key 12 MINUS
+key 13 EQUALS
+key 14 DEL
+key 15 TAB
+key 16 Q
+key 17 W
+key 18 E
+key 19 R
+key 20 T
+key 21 Y
+key 22 U
+key 23 I
+key 24 O
+key 25 P
+key 26 LEFT_BRACKET
+key 27 RIGHT_BRACKET
+key 28 ENTER
+key 29 CTRL_LEFT
+key 30 A
+key 31 S
+key 32 D
+key 33 F
+key 34 G
+key 35 H
+key 36 J
+key 37 K
+key 38 L
+key 39 SEMICOLON
+key 40 APOSTROPHE
+key 41 GRAVE
+key 42 SHIFT_LEFT
+key 43 BACKSLASH
+key 44 Z
+key 45 X
+key 46 C
+key 47 V
+key 48 B
+key 49 N
+key 50 M
+key 51 COMMA
+key 52 PERIOD
+key 53 SLASH
+key 54 SHIFT_RIGHT
+key 56 ALT_LEFT
+key 57 SPACE
+key 58 CAPS_LOCK
+key 59 F1
+key 60 F2
+key 61 F3
+key 62 F4
+key 63 F5
+key 64 F6
+key 65 F7
+key 66 F8
+key 67 F9
+key 68 F10
+key 87 F11
+key 88 F12
+key 97 CTRL_RIGHT
+key 102 HOME
+key 103 DPAD_UP
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+key 107 MOVE_END
+key 108 DPAD_DOWN
+key 111 FORWARD_DEL
+key 113 VOLUME_MUTE
+key 114 VOLUME_DOWN
+key 115 VOLUME_UP
+key 125 MENU
+key 127 SEARCH
+key 163 MEDIA_NEXT
+key 164 MEDIA_PLAY_PAUSE
+key 165 MEDIA_PREVIOUS
+key 166 MEDIA_STOP
+# key 226 tbd reserved key
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2378_Product_1008.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2378_Product_1008.kl
new file mode 100644
index 0000000000..7b19469ab6
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2378_Product_1008.kl
@@ -0,0 +1,40 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# OnLive, Inc. OnLive Wireless Controller, USB adapter
+
+key 164 MEDIA_PLAY_PAUSE
+key 167 MEDIA_RECORD
+key 168 MEDIA_REWIND
+key 208 MEDIA_FAST_FORWARD
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BUTTON_SELECT
+key 316 BUTTON_MODE
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2378_Product_100a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2378_Product_100a.kl
new file mode 100644
index 0000000000..cb2b73afee
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2378_Product_100a.kl
@@ -0,0 +1,40 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# OnLive, Inc. OnLive Wireless Controller
+
+key 164 MEDIA_PLAY_PAUSE
+key 167 MEDIA_RECORD
+key 168 MEDIA_REWIND
+key 208 MEDIA_FAST_FORWARD
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 315 BUTTON_START
+key 314 BUTTON_SELECT
+key 316 BUTTON_MODE
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x03 Z
+axis 0x04 RZ
+axis 0x05 RTRIGGER
+axis 0x02 LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5300.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5300.kl
new file mode 100644
index 0000000000..303e906ad7
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5300.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PowerA MINI PROEX Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5303.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5303.kl
new file mode 100644
index 0000000000..9e98aeec7b
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5303.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Xbox Airflo wired controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_530a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_530a.kl
new file mode 100644
index 0000000000..aa88515c08
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_530a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Xbox 360 Pro EX Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_531a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_531a.kl
new file mode 100644
index 0000000000..09a5c6a85c
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_531a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PowerA Pro Ex
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5397.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5397.kl
new file mode 100644
index 0000000000..66b896a36d
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5397.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# FUS1ON Tournament Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_541a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_541a.kl
new file mode 100644
index 0000000000..24271fbbba
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_541a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PowerA Xbox One Mini Wired Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_542a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_542a.kl
new file mode 100644
index 0000000000..623bd1375b
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_542a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Xbox ONE spectra
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_543a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_543a.kl
new file mode 100644
index 0000000000..59769c4c05
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_543a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PowerA Xbox One wired controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5500.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5500.kl
new file mode 100644
index 0000000000..d76d7d0826
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5500.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Hori XBOX 360 EX 2 with Turbo
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5501.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5501.kl
new file mode 100644
index 0000000000..64d901af13
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5501.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Hori Real Arcade Pro VX-SA
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5506.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5506.kl
new file mode 100644
index 0000000000..bfb23c3090
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5506.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Hori SOULCALIBUR V Stick
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_550d.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_550d.kl
new file mode 100644
index 0000000000..24852b0bd8
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_550d.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Hori GEM Xbox controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_551a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_551a.kl
new file mode 100644
index 0000000000..5e338a5c22
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_551a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PowerA FUSION Pro Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_561a.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_561a.kl
new file mode 100644
index 0000000000..57b7ddcca2
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_561a.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# PowerA FUSION Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5b02.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5b02.kl
new file mode 100644
index 0000000000..bcf354d0e4
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5b02.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Thrustmaster, Inc. GPX Controller
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5d04.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5d04.kl
new file mode 100644
index 0000000000..39d8b2e6c1
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_5d04.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Sabertooth
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_fafe.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_fafe.kl
new file mode 100644
index 0000000000..f8d3f0c080
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_24c6_Product_fafe.kl
@@ -0,0 +1,58 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Rock Candy Gamepad for Xbox 360
+# Autogenerated based on Vendor_045e_Product_02ea.kl (XBox One Controller - Model 1708)
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+
+# Triggers.
+axis 0x02 LTRIGGER
+axis 0x05 RTRIGGER
+
+# Left and right stick.
+# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd.
+# This confuses applications that rely on the flat value because the joystick actually
+# settles in a flat range of +/- 4096 or so.
+axis 0x00 X flat 4096
+axis 0x01 Y flat 4096
+axis 0x03 Z flat 4096
+axis 0x04 RZ flat 4096
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Two overlapping rectangles
+key 314 BUTTON_SELECT
+# Hamburger - 3 parallel lines
+key 315 BUTTON_START
+
+# Xbox key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_27f8_Product_0bbe.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_27f8_Product_0bbe.kl
new file mode 100644
index 0000000000..211e532c83
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_27f8_Product_0bbe.kl
@@ -0,0 +1,54 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Gamevice GV186 Mobile Controller
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 0x133 BUTTON_X
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x134 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+axis 0x00 X
+axis 0x01 Y
+
+axis 0x02 Z
+axis 0x05 RZ
+
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+
+key 0x13d BUTTON_THUMBL
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Left Triangle Button
+key 0x13a BUTTON_SELECT
+# Right Triangle Button
+key 0x13b BUTTON_START
+# Home Button
+key 0x13c BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_27f8_Product_0bbf.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_27f8_Product_0bbf.kl
new file mode 100644
index 0000000000..a59f566384
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_27f8_Product_0bbf.kl
@@ -0,0 +1,54 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Razer Kishi Mobile Controller
+#
+
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 0x133 BUTTON_X
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x134 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+axis 0x00 X
+axis 0x01 Y
+
+axis 0x02 Z
+axis 0x05 RZ
+
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+
+key 0x13d BUTTON_THUMBL
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Left Triangle Button
+key 0x13a BUTTON_SELECT
+# Right Triangle Button
+key 0x13b BUTTON_START
+# Home Button
+key 0x13c BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_28de_Product_1102.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_28de_Product_1102.kl
new file mode 100644
index 0000000000..150a17a782
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_28de_Product_1102.kl
@@ -0,0 +1,74 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Steam Controller - Model 1001 - USB
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 307 BUTTON_X
+key 308 BUTTON_Y
+
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 312 BUTTON_L2
+key 313 BUTTON_R2
+
+# Triggers.
+axis 0x15 LTRIGGER
+axis 0x14 RTRIGGER
+
+# Left and right stick.
+axis 0x00 X
+axis 0x01 Y
+
+# Right stick / mousepad
+axis 0x03 Z
+axis 0x04 RZ
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Hat.
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Dpad (clicks)
+key 544 DPAD_UP
+key 545 DPAD_DOWN
+key 546 DPAD_LEFT
+key 547 DPAD_RIGHT
+
+# Touching the dpad (light touch without pressing down)
+key 289 BUTTON_1
+# Touching the "right stick" / mousepad (light touch without pressing down)
+key 290 BUTTON_2
+
+# Pressing the large paddle on the back, left (linux BTN_WHEEL / BTN_GEAR_DOWN)
+key 336 BUTTON_3
+# Pressing the large paddle on the back, right (linux BTN_GEAR_UP)
+key 337 BUTTON_4
+
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Left arrow
+key 314 BUTTON_SELECT
+# Right arrow
+key 315 BUTTON_START
+
+# Steam key
+key 316 BUTTON_MODE
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2dc8_Product_6101.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2dc8_Product_6101.kl
new file mode 100644
index 0000000000..ec9f5581b0
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2dc8_Product_6101.kl
@@ -0,0 +1,55 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# 8BitDo - SN30 Pro gamepad in Android (D-Input) mode
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Button labeled as "A" but should really produce keycode "B"
+key 304 BUTTON_B
+# Button labeled as "B" but should really produce keycode "A"
+key 305 BUTTON_A
+# Button labeled as "X" but should really produce keycode "Y"
+key 307 BUTTON_Y
+# Button labeled as "Y" but should really produce keycode "X"
+key 308 BUTTON_X
+
+key 310 BUTTON_L1
+key 312 BUTTON_L2
+key 311 BUTTON_R1
+key 313 BUTTON_R2
+
+# Button "Start" does not emit event when gamepad is in Android mode
+# Button "Home"
+key 306 BUTTON_MODE
+key 314 BUTTON_SELECT
+key 315 BUTTON_START
+
+key 317 BUTTON_THUMBL
+key 318 BUTTON_THUMBR
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Dpad
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2e95_Product_7725.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2e95_Product_7725.kl
new file mode 100644
index 0000000000..7672e22f8a
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/Vendor_2e95_Product_7725.kl
@@ -0,0 +1,64 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Scuf Vantage Controller
+#
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x130 BUTTON_X
+# Cross
+key 0x131 BUTTON_A
+# Circle
+key 0x132 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x134 BUTTON_L1
+key 0x135 BUTTON_R1
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+
+# L2 Trigger axis
+axis 0x03 LTRIGGER
+# R2 Trigger axis
+axis 0x04 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 0x13a BUTTON_THUMBL
+# Right stick click
+key 0x13b BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share
+key 0x138 BUTTON_SELECT
+# Options
+key 0x139 BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+# Touchpad press
+key 0x13d BUTTON_1
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/idroid_con.kl b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/idroid_con.kl
new file mode 100644
index 0000000000..6d4a163d9f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/test_data/pixel-7/idroid_con.kl
@@ -0,0 +1,37 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# SnakeByte iDroid:con
+
+key 304 BUTTON_A
+key 305 BUTTON_B
+key 306 BUTTON_THUMBL
+key 307 BUTTON_X
+key 308 BUTTON_Y
+key 309 BUTTON_THUMBR
+key 310 BUTTON_L1
+key 311 BUTTON_R1
+key 312 BUTTON_L2
+key 313 BUTTON_R2
+key 314 BUTTON_SELECT
+key 315 BUTTON_START
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x03 RX
+axis 0x04 RY
+axis 0x05 RZ
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
diff --git a/evdev/src/main/rust/evdev_manager/core/tests/tokenizer_test.rs b/evdev/src/main/rust/evdev_manager/core/tests/tokenizer_test.rs
new file mode 100644
index 0000000000..09a7ab6360
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/core/tests/tokenizer_test.rs
@@ -0,0 +1,145 @@
+use evdev_manager_core::android::keylayout::tokenizer::Tokenizer;
+#[cfg(test)]
+use pretty_assertions::assert_eq;
+use std::path::PathBuf;
+
+#[test]
+fn test_is_eof() {
+ let tokenizer = Tokenizer::from_contents(PathBuf::new(), "");
+ assert!(tokenizer.is_eof());
+
+ let tokenizer = Tokenizer::from_contents(PathBuf::new(), "a");
+ assert!(!tokenizer.is_eof());
+}
+
+#[test]
+fn test_is_eol() {
+ let tokenizer = Tokenizer::from_contents(PathBuf::new(), "");
+ assert!(tokenizer.is_eol());
+
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "a\nb");
+ assert!(!tokenizer.is_eol());
+ tokenizer.next_char();
+ assert!(tokenizer.is_eol());
+}
+
+#[test]
+fn test_peek_char() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "abc");
+ assert_eq!(tokenizer.peek_char(), 'a');
+ assert_eq!(tokenizer.peek_char(), 'a'); // Should not advance
+
+ tokenizer.next_char();
+ assert_eq!(tokenizer.peek_char(), 'b');
+}
+
+#[test]
+fn test_next_char() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "abc");
+ assert_eq!(tokenizer.next_char(), 'a');
+ assert_eq!(tokenizer.next_char(), 'b');
+ assert_eq!(tokenizer.next_char(), 'c');
+ assert_eq!(tokenizer.next_char(), '\0');
+}
+
+#[test]
+fn test_next_token() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "key 1 ESCAPE");
+ assert_eq!(tokenizer.next_token(" \t"), "key");
+ assert_eq!(tokenizer.next_token(" \t"), "1");
+ assert_eq!(tokenizer.next_token(" \t"), "ESCAPE");
+ assert_eq!(tokenizer.next_token(" \t"), "");
+}
+
+#[test]
+fn test_next_token_with_multiple_delimiters() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "key\t1\nESCAPE");
+ assert_eq!(tokenizer.next_token(" \t"), "key");
+ assert_eq!(tokenizer.next_token(" \t"), "1");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.next_token(" \t"), "ESCAPE");
+}
+
+#[test]
+fn test_next_line() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "line1\nline2\nline3");
+ assert_eq!(tokenizer.peek_remainder_of_line(), "line1");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.peek_remainder_of_line(), "line2");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.peek_remainder_of_line(), "line3");
+}
+
+#[test]
+fn test_skip_delimiters() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), " key");
+ tokenizer.skip_delimiters(" \t");
+ assert_eq!(tokenizer.peek_char(), 'k');
+}
+
+#[test]
+fn test_get_location() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new().join("test.txt"), "line1\nline2");
+ assert_eq!(tokenizer.get_location(), "test.txt:1");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.get_location(), "test.txt:2");
+}
+
+#[test]
+fn test_peek_remainder_of_line() {
+ let tokenizer = Tokenizer::from_contents(PathBuf::new(), "key 1 ESCAPE\nnext line");
+ assert_eq!(tokenizer.peek_remainder_of_line(), "key 1 ESCAPE");
+
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "key 1 ESCAPE\nnext line");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.peek_remainder_of_line(), "next line");
+}
+
+#[test]
+fn test_handles_comments() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "key 1 ESCAPE # comment");
+ assert_eq!(tokenizer.next_token(" \t"), "key");
+ assert_eq!(tokenizer.next_token(" \t"), "1");
+ assert_eq!(tokenizer.next_token(" \t"), "ESCAPE");
+ assert_eq!(tokenizer.peek_char(), ' ');
+ tokenizer.skip_delimiters(" \t");
+ assert_eq!(tokenizer.peek_char(), '#');
+}
+
+#[test]
+fn test_handles_empty_lines() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "line1\n\nline3");
+ assert_eq!(tokenizer.peek_remainder_of_line(), "line1");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.peek_remainder_of_line(), "");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.peek_remainder_of_line(), "line3");
+}
+
+#[test]
+fn test_handles_whitespace_only_lines() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "line1\n \nline3");
+ assert_eq!(tokenizer.peek_remainder_of_line(), "line1");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.peek_remainder_of_line(), " ");
+ tokenizer.next_line();
+ assert_eq!(tokenizer.peek_remainder_of_line(), "line3");
+}
+
+#[test]
+fn test_multiple_lines_sequential() {
+ let mut tokenizer = Tokenizer::from_contents(PathBuf::new(), "key 1 A\nkey 2 B\nkey 3 C");
+ assert_eq!(tokenizer.next_token(" \t"), "key");
+ assert_eq!(tokenizer.next_token(" \t"), "1");
+ assert_eq!(tokenizer.next_token(" \t"), "A");
+ tokenizer.next_line();
+
+ assert_eq!(tokenizer.next_token(" \t"), "key");
+ assert_eq!(tokenizer.next_token(" \t"), "2");
+ assert_eq!(tokenizer.next_token(" \t"), "B");
+ tokenizer.next_line();
+
+ assert_eq!(tokenizer.next_token(" \t"), "key");
+ assert_eq!(tokenizer.next_token(" \t"), "3");
+ assert_eq!(tokenizer.next_token(" \t"), "C");
+}
diff --git a/evdev/src/main/rust/evdev_manager/jni/Cargo.toml b/evdev/src/main/rust/evdev_manager/jni/Cargo.toml
new file mode 100644
index 0000000000..932490aa99
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/jni/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "evdev_manager_jni"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "evdev_manager_jni"
+crate-type = ["rlib"]
+
+[dependencies]
+jni = "0.21.1"
+log = "0.4.28"
+evdev = { path = "../../evdev" }
+evdev_manager_core = { path = "../core" }
+libc = "0.2.177"
diff --git a/evdev/src/main/rust/evdev_manager/jni/build.rs b/evdev/src/main/rust/evdev_manager/jni/build.rs
new file mode 100644
index 0000000000..be1f4d1098
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/jni/build.rs
@@ -0,0 +1,16 @@
+fn main() {
+ // This crate now uses pure JNI without C++ Binder layer.
+ // No C++ compilation needed.
+
+ // Just check that we're building for Android
+ let target = std::env::var("TARGET").expect("TARGET environment variable not set");
+ let is_android = target.contains("android");
+
+ if !is_android {
+ eprintln!(
+ "Warning: Building for non-Android target '{}'. This crate is designed for Android.",
+ target
+ );
+ eprintln!("Use Gradle for actual builds.");
+ }
+}
diff --git a/evdev/src/main/rust/evdev_manager/jni/src/evdev_jni_observer.rs b/evdev/src/main/rust/evdev_manager/jni/src/evdev_jni_observer.rs
new file mode 100644
index 0000000000..5bf31d541f
--- /dev/null
+++ b/evdev/src/main/rust/evdev_manager/jni/src/evdev_jni_observer.rs
@@ -0,0 +1,280 @@
+use evdev::{util::event_code_to_int, InputEvent};
+use evdev_manager_core::android::android_codes;
+use evdev_manager_core::android::android_codes::AKEYCODE_UNKNOWN;
+use evdev_manager_core::android::keylayout::key_layout_map_manager::KeyLayoutMapManager;
+use evdev_manager_core::evdev_device_info::EvdevDeviceInfo;
+use evdev_manager_core::grabbed_device_handle::GrabbedDeviceHandle;
+use jni::objects::{GlobalRef, JValue};
+use jni::JavaVM;
+use std::process;
+use std::sync::{Arc, Mutex};
+
+pub struct EvdevJniObserver {
+ jvm: Arc,
+ system_bridge: GlobalRef,
+ key_layout_map_manager: Arc