diff --git a/.editorconfig b/.editorconfig
index bb6ba13598..0c79c6b37b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,4 +1,5 @@
[*.{kt,kts}]
+ktlint_standard_function-expression-body = disabled
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_ignore_back_ticked_identifier = true
ktlint_code_style = intellij_idea # Use IntelliJ style because it has trailing commas
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 6b425409b8..b76001ca56 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -2,16 +2,14 @@
name: Bug report
about: Create a report to help us improve
title: ''
-labels: bug
+labels: bug, needs triage
assignees: sds100
---
**Developer TODO (don't remove)**
-- [ ] create new branch. put issue number at start of name if not a very quick fix.
- [ ] write tests. put issue number in comment
- [ ] update documentation
-- [ ] merge and delete branch (don't squash because want commit history to see why I made changes)
**Discord message link/email recipient**
diff --git a/.github/ISSUE_TEMPLATE/new-feature.md b/.github/ISSUE_TEMPLATE/new-feature.md
index e794ff0142..677a2e1159 100644
--- a/.github/ISSUE_TEMPLATE/new-feature.md
+++ b/.github/ISSUE_TEMPLATE/new-feature.md
@@ -2,12 +2,10 @@
name: New Feature
about: Add a new feature or enhancement to the app.
title: ''
-labels: enhancement
+labels: enhancement, needs triage
assignees: sds100
---
**Developer TODO (don't remove)**
-- [ ] create new branch. put issue number at start of name
- [ ] update documentation
-- [ ] merge and delete branch (don't squash because want commit history to see why I made changes)
diff --git a/.github/workflows/build-mkdocs.yml b/.github/workflows/build-mkdocs.yml
index 752f6a711a..d97c50f25c 100644
--- a/.github/workflows/build-mkdocs.yml
+++ b/.github/workflows/build-mkdocs.yml
@@ -14,7 +14,7 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.x
diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml
deleted file mode 100644
index 23d9bc1dbf..0000000000
--- a/.github/workflows/production.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-name: Production - build and release app
-
-on:
- push:
- branches:
- - 'master'
-
-concurrency:
- group: ${{ github.workflow }}
- cancel-in-progress: true
-
-jobs:
- apk:
- name: Build and release to production
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- - uses: actions/cache@v3
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
-
- - name: set up JDK 17
- uses: actions/setup-java@v3
- with:
- distribution: 'oracle'
- java-version: 17
- cache: 'gradle'
-
- - name: Setup Android SDK
- uses: android-actions/setup-android@v2
-
- - name: set up Ruby for fastlane
- uses: ruby/setup-ruby@v1
- with:
- ruby-version: '3.3'
-
- - name: Install bundle
- run: bundle install
-
- - name: Create keystore
- env:
- KEYSTORE: ${{ secrets.KEYSTORE }}
- run: echo "$KEYSTORE" | base64 --decode > app/keystore.jks
-
- - name: Create Google Play service account key
- env:
- GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
- run: echo "$GOOGLE_PLAY_SERVICE_ACCOUNT" | base64 --decode > app/play-service-account-key.json
-
- - name: Build apk with fastlane
- run: bundle exec fastlane prod github_token:${{ secrets.GITHUB_TOKEN }}
- env:
- KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
- KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 2356ae0a8a..8c1ab5cfbd 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -4,12 +4,31 @@ on:
pull_request:
jobs:
+ test:
+ name: Run unit tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'oracle'
+ java-version: 17
+ cache: 'gradle'
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v2
+
+ - name: Unit tests
+ run: bash ./gradlew testDebugUnitTest
+
style:
name: Code style check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- uses: actions/cache@v3
with:
@@ -38,7 +57,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- uses: actions/cache@v3
with:
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index dc882a2172..2ced70cf71 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -10,12 +10,31 @@ concurrency:
cancel-in-progress: true
jobs:
+ test:
+ name: Run unit tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'oracle'
+ java-version: 17
+ cache: 'gradle'
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v2
+
+ - name: Unit tests
+ run: bash ./gradlew testDebugUnitTest
+
style:
name: Code style check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- uses: actions/cache@v3
with:
@@ -44,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- uses: actions/cache@v3
with:
@@ -97,7 +116,7 @@ jobs:
run: echo "APK_NAME=$(basename app/build/outputs/apk/ci/*.apk .apk)" >> $GITHUB_ENV
- name: Upload APK
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_NAME }}
path: app/build/outputs/apk/ci/${{ env.APK_NAME }}.apk
@@ -107,7 +126,7 @@ jobs:
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
- args: app/build/outputs/apk/ci/${{ env.APK_NAME }}.apk
+ args: app/build/outputs/apk/free/ci/${{ env.APK_NAME }}.apk
- name: Report build status to Discord
uses: sarisia/actions-status-discord@v1
diff --git a/.idea/runConfigurations/app.xml b/.idea/runConfigurations/app.xml
new file mode 100644
index 0000000000..772f9e4c1b
--- /dev/null
+++ b/.idea/runConfigurations/app.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a455836e2f..8bb18a10d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+## [2.7.0](https://github.com/sds100/KeyMapper/releases/tag/v2.7.0)
+
+#### 8 December 2024
+
+## Added
+
+- #1274 New trigger! You can now trigger your key maps from any of the ways your phone launches the assistant! This could be the Bixby button, Power button, or a button on your headset.
+- #1304 Vietnamese translations.
+
+## Bug fixes
+
+- #1222 #1307 Key Mapper doesn't execute the correct app shortcut action if you created multiple from the same app.
+- #1328 Single-character non-ASCII TEXT_BLOCK input crashes the service
+
## [2.6.2](https://github.com/sds100/KeyMapper/releases/tag/v2.6.2)
#### 9 September 2024
diff --git a/README.md b/README.md
index 8b17a7a0b5..892afcb8be 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-**Project in maintenance mode.**
+**Project under slower development.**
> Well, working on this project was a fun ride 🎢! This project has taught me so much about Android, software development and how to collaborate with an online community. It has been my dream to lead a big FOSS project with people from all over the world so a **huge** thank you goes to everyone that spread the word and helped on GitHub along the way ☺. Unfortunately, I do not have any more time to work on this project - I'm now studying Computer Science at university and I have landed software-dev side jobs, which has taken up any free-time I did have to code on the side.
>
> A special thank you goes to everyone in the [Team](https://docs.keymapper.club/#our-team) for their long-term
@@ -27,14 +27,17 @@ Key Mapper is a free and open source Android app that can remap your buttons and
🎉 Check out the [website](https://docs.keymapper.club) for more information and help! 🎉
+
## Translations
-![cs translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Czech&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27cs%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![es-ES translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Spanish&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27es-ES%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Polish&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27pl%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Russian&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ru%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![sk translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Slovak&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27sk%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![zh-CN translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Chinese%20(Simplified)&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27zh-CN%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
+[![cs proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=cs&style=flat&logo=crowdin&query=%24.progress.1.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![es-ES proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=es-ES&style=flat&logo=crowdin&query=%24.progress.3.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![pl proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=pl&style=flat&logo=crowdin&query=%24.progress.8.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![pt-BR proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=pt-BR&style=flat&logo=crowdin&query=%24.progress.9.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![ru proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=ru&style=flat&logo=crowdin&query=%24.progress.10.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![sk proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=sk&style=flat&logo=crowdin&query=%24.progress.11.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![zh-CN proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=zh-CN&style=flat&logo=crowdin&query=%24.progress.15.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+
## Star History
diff --git a/app/build.gradle b/app/build.gradle
index 734ec6a343..c901829a9e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,6 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-kapt'
+apply plugin: "com.android.application"
+apply plugin: "kotlin-android"
+apply plugin: "kotlin-kapt"
apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: "kotlinx-serialization"
apply plugin: "org.jetbrains.kotlin.plugin.parcelize"
@@ -8,9 +8,9 @@ apply plugin: "org.jlleitschuh.gradle.ktlint"
android {
- namespace 'io.github.sds100.keymapper'
+ namespace "io.github.sds100.keymapper"
compileSdk 34
- buildToolsVersion = '34.0.0'
+ buildToolsVersion = "34.0.0"
def versionProperties = new Properties()
file("version.properties").withInputStream { versionProperties.load(it) }
@@ -46,7 +46,7 @@ android {
release {
minifyEnabled true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release
}
@@ -55,19 +55,32 @@ android {
versionNameSuffix "-debug"
}
+ debug_release {
+ // Extend from debug build type so compose Live Edit and rapid building works
+ initWith debug
+
+ // Do not alter the package name so can test revenuecat and billing while developing.
+ applicationIdSuffix ""
+
+ /*
+ This is required because the splitties library does not have a debug_release build type.
+ */
+ matchingFallbacks = ["debug"]
+ }
+
ci {
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode true
- proguardFiles 'proguard-rules.pro'
+ proguardFiles "proguard-rules.pro"
}
/*
- This is required because the splitties library doesn't have a ci build type.
+ This is required because the splitties library does not have a ci build type.
*/
- matchingFallbacks = ['debug']
+ matchingFallbacks = ["debug"]
applicationIdSuffix ".ci"
versionNameSuffix "-ci." + versionProperties.getProperty("VERSION_NUM")
@@ -75,11 +88,30 @@ android {
}
}
+ flavorDimensions = ["pro"]
+ productFlavors {
+ free {
+ dimension "pro"
+ }
+ pro {
+ dimension "pro"
+
+ File file = rootProject.file("local.properties")
+
+ if (file.exists()) {
+ def localProperties = new Properties()
+ localProperties.load(new FileInputStream(file))
+ buildConfigField("String", "REVENUECAT_API_KEY", localProperties["REVENUECAT_API_KEY"])
+ }
+ }
+ }
+
buildFeatures {
dataBinding true
viewBinding true
aidl true
buildConfig true
+ compose true
}
compileOptions {
@@ -95,39 +127,42 @@ android {
kapt {
correctErrorTypes = true
}
-}
-android.sourceSets {
- test {
- java.srcDirs += "$projectDir/src/testShared"
+ composeOptions {
+ kotlinCompilerExtensionVersion "1.5.10"
}
- androidTest {
- java.srcDirs += "$projectDir/src/testShared"
- assets.srcDirs += files("$projectDir/schemas".toString())
- resources.srcDirs += ['src/test/resources']
+ sourceSets {
+ androidTest {
+ assets.srcDirs += files("$projectDir/schemas".toString())
+ resources.srcDirs += ["src/test/resources"]
+ }
+
+ test {
+ java.srcDirs += ["src/pro/test/java"]
+ }
}
-}
-android.applicationVariants.all { variant ->
- variant.outputs.all {
- outputFileName = "keymapper-${variant.versionName}.apk"
+ applicationVariants.configureEach { variant ->
+ variant.outputs.configureEach {
+ outputFileName = "keymapper-${variant.versionName}.apk"
+ }
}
}
dependencies {
- implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation fileTree(include: ["*.jar"], dir: "libs")
- compileOnly project(':systemstubs')
+ compileOnly project(":systemstubs")
def room_version = "2.6.1"
- def coroutinesVersion = "1.5.0"
- def nav_version = '2.7.7'
+ def coroutinesVersion = "1.9.0"
+ def nav_version = '2.8.4'
def work_version = "2.9.1"
def epoxy_version = "4.6.2"
def splitties_version = "3.0.0"
def multidex_version = "2.0.1"
- def shizuku_version = '13.1.5'
+ def shizuku_version = "13.1.5"
// kotlin stuff
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
@@ -135,21 +170,23 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
// random stuff
- implementation 'com.google.android.material:material:1.12.0'
- implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
+ implementation "com.google.android.material:material:1.12.0"
+ implementation "com.github.salomonbrys.kotson:kotson:2.5.0"
implementation "com.airbnb.android:epoxy:$epoxy_version"
- implementation 'com.github.AppIntro:AppIntro:6.1.0'
+ implementation "com.github.AppIntro:AppIntro:6.1.0"
implementation "com.airbnb.android:epoxy-databinding:$epoxy_version"
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
- implementation 'com.jakewharton.timber:timber:4.7.1'
- implementation 'uk.co.samuelwall:material-tap-target-prompt:3.1.0'
- implementation 'net.lingala.zip4j:zip4j:2.8.0'
+ implementation "com.jakewharton.timber:timber:4.7.1"
+ implementation "uk.co.samuelwall:material-tap-target-prompt:3.1.0"
+ implementation "net.lingala.zip4j:zip4j:2.8.0"
implementation "com.anggrayudi:storage:0.8.1"
- implementation 'com.github.MFlisar:DragSelectRecyclerView:0.3'
- implementation 'com.google.android.flexbox:flexbox:3.0.0'
+ implementation "com.github.MFlisar:DragSelectRecyclerView:0.3"
+ implementation "com.google.android.flexbox:flexbox:3.0.0"
implementation "dev.rikka.shizuku:api:$shizuku_version"
implementation "dev.rikka.shizuku:provider:$shizuku_version"
- implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
+ implementation "org.lsposed.hiddenapibypass:hiddenapibypass:4.3"
+ proImplementation 'com.revenuecat.purchases:purchases:8.8.1'
+
// splitties
implementation "com.louiscad.splitties:splitties-bitflags:$splitties_version"
@@ -160,23 +197,23 @@ dependencies {
implementation "com.louiscad.splitties:splitties-mainthread:$splitties_version"
// androidx
- implementation 'androidx.legacy:legacy-support-core-ui:1.0.0'
- implementation 'androidx.core:core-ktx:1.13.1'
-
- implementation 'androidx.activity:activity-ktx:1.9.1'
- implementation 'androidx.fragment:fragment-ktx:1.8.2'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
+ implementation "androidx.legacy:legacy-support-core-ui:1.0.0"
+ implementation "androidx.core:core-ktx:1.13.1"
+
+ implementation "androidx.activity:activity-ktx:1.9.3"
+ implementation "androidx.fragment:fragment-ktx:1.8.5"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7"
implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.multidex:multidex:$multidex_version"
- implementation 'androidx.appcompat:appcompat:1.7.0'
- implementation 'androidx.recyclerview:recyclerview:1.3.2'
- implementation 'androidx.preference:preference-ktx:1.2.1'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation "androidx.appcompat:appcompat:1.7.0"
+ implementation "androidx.recyclerview:recyclerview:1.3.2"
+ implementation "androidx.preference:preference-ktx:1.2.1"
+ implementation "androidx.constraintlayout:constraintlayout:2.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.viewpager2:viewpager2:1.1.0"
@@ -184,7 +221,15 @@ dependencies {
implementation "androidx.core:core-splashscreen:1.0.1"
kapt "androidx.room:room-compiler:$room_version"
-// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
+ // Compose
+ implementation "androidx.compose.ui:ui-android:1.7.5"
+ implementation "androidx.compose.material3:material3-android:1.3.1"
+ implementation "androidx.compose.ui:ui-tooling-preview-android:1.7.5"
+ implementation "androidx.compose.material:material-icons-extended-android:1.7.5"
+ debugImplementation "androidx.compose.ui:ui-tooling:1.7.5"
+ debug_releaseImplementation "androidx.compose.ui:ui-tooling:1.7.5"
+
+// debugImplementation "com.squareup.leakcanary:leakcanary-android:2.6"
def junitVersion = "4.13.2"
def androidXTestExtKotlinRunnerVersion = "1.2.1"
@@ -202,7 +247,7 @@ dependencies {
testImplementation "pl.pragmatists:JUnitParams:1.1.1"
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
testImplementation "org.mockito:mockito-core:5.1.1"
- testImplementation 'org.mockito:mockito-inline:5.0.0'
+ testImplementation "org.mockito:mockito-inline:5.0.0"
androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
@@ -210,8 +255,8 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
- androidTestImplementation 'android.arch.persistence.room:testing:1.1.1'
+ androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
androidTestImplementation "org.mockito:mockito-android:4.6.1"
- debugImplementation "androidx.fragment:fragment-testing:1.8.2"
+ debugImplementation "androidx.fragment:fragment-testing:1.8.5"
implementation "androidx.test:core:$androidXTestCoreVersion"
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/io/github/sds100/keymapper/AppDatabaseMigrationTest.kt b/app/src/androidTest/java/io/github/sds100/keymapper/AppDatabaseMigrationTest.kt
index a9d25db0db..83dc9f3ce9 100644
--- a/app/src/androidTest/java/io/github/sds100/keymapper/AppDatabaseMigrationTest.kt
+++ b/app/src/androidTest/java/io/github/sds100/keymapper/AppDatabaseMigrationTest.kt
@@ -11,8 +11,12 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.github.salomonbrys.kotson.get
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonParseException
+import com.google.gson.JsonParser
import io.github.sds100.keymapper.data.db.AppDatabase
-import io.github.sds100.keymapper.util.JsonTestUtils
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.runBlocking
diff --git a/app/src/androidTest/java/io/github/sds100/keymapper/JsonTestUtils.kt b/app/src/androidTest/java/io/github/sds100/keymapper/JsonTestUtils.kt
new file mode 100644
index 0000000000..d79454b9df
--- /dev/null
+++ b/app/src/androidTest/java/io/github/sds100/keymapper/JsonTestUtils.kt
@@ -0,0 +1,88 @@
+package io.github.sds100.keymapper
+
+import com.github.salomonbrys.kotson.contains
+import com.github.salomonbrys.kotson.forEach
+import com.github.salomonbrys.kotson.get
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.core.Is.`is`
+import org.junit.Assert
+
+/**
+ * Created by sds100 on 25/01/21.
+ */
+object JsonTestUtils {
+ private const val NAME_SEPARATOR = '/'
+
+ fun compareBothWays(element: JsonElement, elementName: String, other: JsonElement, otherName: String) {
+ compare("", element, elementName, other, otherName)
+ compare("", other, elementName, element, elementName)
+ }
+
+ private fun compare(parentNamePath: String = "", element: JsonElement, elementName: String, rootToCompare: JsonElement, rootName: String) {
+ when (element) {
+ is JsonObject -> {
+ element.forEach { name, jsonElement ->
+ val newPath = if (parentNamePath.isBlank()) {
+ name
+ } else {
+ "$parentNamePath$NAME_SEPARATOR$name"
+ }
+
+ compare(newPath, jsonElement, elementName, rootToCompare, rootName)
+ }
+ }
+
+ is JsonArray -> {
+ val pathToArrayToCompare = parentNamePath.split(NAME_SEPARATOR)
+ var arrayToCompare: JsonArray? = null
+
+ var parentElement: JsonElement = rootToCompare
+ pathToArrayToCompare.forEach {
+ if (it == "") return@forEach
+
+ parentElement = parentElement[it]
+ }
+
+ if (parentElement is JsonArray) {
+ arrayToCompare = parentElement as JsonArray
+ }
+
+ Assert.assertNotNull("can't find array $elementName/$parentNamePath in $rootName", arrayToCompare)
+ arrayToCompare ?: return
+
+ element.forEachIndexed { index, arrayElement ->
+ val validIndex = index <= arrayToCompare.toList().lastIndex
+
+ assertThat("$rootName/${pathToArrayToCompare.last()} doesn't contain $arrayElement at $index index", validIndex)
+
+ compare("", arrayElement, "$elementName/${pathToArrayToCompare.last()}", arrayToCompare[index]!!, "$rootName/${pathToArrayToCompare.last()}")
+ }
+ }
+
+ is JsonPrimitive -> {
+ val names = parentNamePath.split(NAME_SEPARATOR)
+ var parentElement: JsonElement = rootToCompare
+
+ if (names == listOf("")) {
+ assertThat("$elementName/:$element doesn't match $rootName/:$parentElement", (parentElement), `is`(element))
+ } else {
+ names.forEachIndexed { index, name ->
+ if (parentElement is JsonObject) {
+ assertThat("$elementName/$parentNamePath not found in $rootName", (parentElement as JsonObject).contains(name))
+ }
+
+ parentElement = parentElement[name]
+
+ if (index == names.lastIndex) {
+ assertThat("$elementName/$parentNamePath:$element doesn't match $rootName/$parentNamePath:$parentElement", (parentElement as JsonPrimitive), `is`(element))
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/free/java/io/github/sds100/keymapper/MainActivity.kt b/app/src/free/java/io/github/sds100/keymapper/MainActivity.kt
new file mode 100644
index 0000000000..51dddd9b2e
--- /dev/null
+++ b/app/src/free/java/io/github/sds100/keymapper/MainActivity.kt
@@ -0,0 +1,3 @@
+package io.github.sds100.keymapper
+
+class MainActivity : BaseMainActivity()
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt
similarity index 85%
rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt
rename to app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt
index 39e497cebd..8516e81376 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt
+++ b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt
@@ -9,12 +9,14 @@ import io.github.sds100.keymapper.actions.PerformActionsUseCase
import io.github.sds100.keymapper.constraints.ConstraintSnapshot
import io.github.sds100.keymapper.constraints.ConstraintState
import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase
+import io.github.sds100.keymapper.constraints.isSatisfied
import io.github.sds100.keymapper.data.PreferenceDefaults
import io.github.sds100.keymapper.data.entities.ActionEntity
import io.github.sds100.keymapper.mappings.ClickType
import io.github.sds100.keymapper.mappings.keymaps.KeyMap
import io.github.sds100.keymapper.mappings.keymaps.KeyMapAction
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTrigger
+import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode
@@ -56,7 +58,7 @@ class KeyMapController(
* @return whether the actions assigned to this trigger will be performed on the down event of the final key
* rather than the up event.
*/
- fun performActionOnDown(trigger: KeyMapTrigger): Boolean = (
+ fun performActionOnDown(trigger: Trigger): Boolean = (
trigger.keys.size <= 1 &&
trigger.keys.getOrNull(0)?.clickType != ClickType.DOUBLE_PRESS &&
trigger.mode == TriggerMode.Undefined
@@ -89,13 +91,13 @@ class KeyMapController(
} else {
detectKeyMaps = true
- val longPressSequenceTriggerKeys = mutableListOf()
+ val longPressSequenceTriggerKeys = mutableListOf()
val doublePressKeys = mutableListOf()
setActionMapAndOptions(value.flatMap { it.actionList }.toSet())
- val triggers = mutableListOf()
+ val triggers = mutableListOf()
val sequenceTriggers = mutableListOf()
val parallelTriggers = mutableListOf()
@@ -109,47 +111,51 @@ class KeyMapController(
val parallelTriggerModifierKeyIndices = mutableListOf>()
// Only process key maps that can be triggered
- val validKeyMaps = value.filter {
- it.actionList.isNotEmpty() && it.isEnabled
+ val validKeyMaps = value.filter { keyMap ->
+ keyMap.actionList.isNotEmpty() &&
+ keyMap.isEnabled &&
+ keyMap.trigger.keys.all { it is KeyCodeTriggerKey }
}
for ((triggerIndex, keyMap) in validKeyMaps.withIndex()) {
// TRIGGER STUFF
- keyMap.trigger.keys.forEachIndexed { keyIndex, key ->
- if (keyMap.trigger.mode == TriggerMode.Sequence &&
- key.clickType == ClickType.LONG_PRESS
- ) {
-
- if (keyMap.trigger.keys.size > 1) {
- longPressSequenceTriggerKeys.add(key)
+ keyMap.trigger.keys
+ .mapNotNull { it as? KeyCodeTriggerKey }
+ .forEachIndexed { keyIndex, key ->
+ if (keyMap.trigger.mode == TriggerMode.Sequence &&
+ key.clickType == ClickType.LONG_PRESS
+ ) {
+
+ if (keyMap.trigger.keys.size > 1) {
+ longPressSequenceTriggerKeys.add(key)
+ }
}
- }
- if ((
- keyMap.trigger.mode == TriggerMode.Sequence ||
- keyMap.trigger.mode == TriggerMode.Undefined
- ) &&
- key.clickType == ClickType.DOUBLE_PRESS
- ) {
- doublePressKeys.add(TriggerKeyLocation(triggerIndex, keyIndex))
- }
-
- when (key.device) {
- TriggerKeyDevice.Internal -> {
- detectInternalEvents = true
+ if ((
+ keyMap.trigger.mode == TriggerMode.Sequence ||
+ keyMap.trigger.mode == TriggerMode.Undefined
+ ) &&
+ key.clickType == ClickType.DOUBLE_PRESS
+ ) {
+ doublePressKeys.add(TriggerKeyLocation(triggerIndex, keyIndex))
}
- TriggerKeyDevice.Any -> {
- detectInternalEvents = true
- detectExternalEvents = true
- }
+ when (key.device) {
+ TriggerKeyDevice.Internal -> {
+ detectInternalEvents = true
+ }
+
+ TriggerKeyDevice.Any -> {
+ detectInternalEvents = true
+ detectExternalEvents = true
+ }
- is TriggerKeyDevice.External -> {
- detectExternalEvents = true
+ is TriggerKeyDevice.External -> {
+ detectExternalEvents = true
+ }
}
}
- }
val encodedActionList = encodeActionList(keyMap.actionList)
@@ -324,14 +330,16 @@ class KeyMapController(
}
}
- parallelTriggers.forEach { triggerIndex ->
+ for (triggerIndex in parallelTriggers) {
val trigger = triggers[triggerIndex]
- trigger.keys.forEachIndexed { keyIndex, key ->
- if (isModifierKey(key.keyCode)) {
- parallelTriggerModifierKeyIndices.add(triggerIndex to keyIndex)
+ trigger.keys
+ .mapNotNull { it as? KeyCodeTriggerKey }
+ .forEachIndexed { keyIndex, key ->
+ if (isModifierKey(key.keyCode)) {
+ parallelTriggerModifierKeyIndices.add(triggerIndex to keyIndex)
+ }
}
- }
}
reset()
@@ -385,13 +393,13 @@ class KeyMapController(
/**
* All sequence events that have the long press click type.
*/
- private var longPressSequenceTriggerKeys: Array = arrayOf()
+ private var longPressSequenceTriggerKeys: Array = arrayOf()
/**
* All double press keys and the index of their corresponding trigger. first is the event and second is
* the trigger index.
*/
- private var doublePressTriggerKeys: Array = arrayOf()
+ private var doublePressTriggerKeys: Array = arrayOf()
/**
* order matches with [doublePressTriggerKeys]
@@ -405,8 +413,8 @@ class KeyMapController(
*/
private var doublePressTimeoutTimes = longArrayOf()
- private var actionMap = SparseArrayCompat()
- private var triggers: Array = emptyArray()
+ private var actionMap: SparseArrayCompat = SparseArrayCompat()
+ private var triggers: Array = emptyArray()
/**
* The events to detect for each sequence trigger.
@@ -424,9 +432,9 @@ class KeyMapController(
/**
* The indexes of triggers that overlap after the first element with each trigger in [sequenceTriggers]
*/
- private var sequenceTriggersOverlappingSequenceTriggers = arrayOf()
+ private var sequenceTriggersOverlappingSequenceTriggers: Array = arrayOf()
- private var sequenceTriggersOverlappingParallelTriggers = arrayOf()
+ private var sequenceTriggersOverlappingParallelTriggers: Array = arrayOf()
/**
* An array of the index of the last matched event in each trigger.
@@ -436,7 +444,7 @@ class KeyMapController(
/**
* An array of the constraints for every trigger
*/
- private var triggerConstraints: Array = arrayOf()
+ private var triggerConstraints: Array = arrayOf()
/**
* The events to detect for each parallel trigger.
@@ -447,7 +455,7 @@ class KeyMapController(
* The actions to perform when each trigger is detected. The order matches with
* [triggers].
*/
- private var triggerActions: Array = arrayOf()
+ private var triggerActions: Array = arrayOf()
/**
* Stores whether each event in each parallel trigger need to be released after being held down.
@@ -570,14 +578,18 @@ class KeyMapController(
metaStateFromKeyEvent = metaState
// remove the metastate from any modifier keys that remapped and are pressed down
- parallelTriggerModifierKeyIndices.forEach {
+ for (it in parallelTriggerModifierKeyIndices) {
val triggerIndex = it.first
val eventIndex = it.second
- val event = triggers[triggerIndex].keys[eventIndex]
+ val key = triggers[triggerIndex].keys[eventIndex]
+
+ if (key !is KeyCodeTriggerKey) {
+ continue
+ }
if (parallelTriggerEventsAwaitingRelease[triggerIndex][eventIndex]) {
metaStateFromKeyEvent =
- metaStateFromKeyEvent.minusFlag(KeyEventUtils.modifierKeycodeToMetaState(event.keyCode))
+ metaStateFromKeyEvent.minusFlag(KeyEventUtils.modifierKeycodeToMetaState(key.keyCode))
}
}
@@ -612,38 +624,63 @@ class KeyMapController(
val constraintSnapshot: ConstraintSnapshot by lazy { detectConstraints.getSnapshot() }
+ /**
+ * Store which triggers are currently satisfied by the constraints.
+ * This is used to check later on whether to wait for a double press to complete
+ * before executing a short press. See issue #1271.
+ */
+ val triggersSatisfiedByConstraints = mutableSetOf()
+
+ for (triggerIndex in parallelTriggers.plus(sequenceTriggers)) {
+ val constraintState = triggerConstraints[triggerIndex]
+
+ if (constraintSnapshot.isSatisfied(constraintState)) {
+ triggersSatisfiedByConstraints.add(triggerIndex)
+ }
+ }
+
// consume sequence trigger keys until their timeout has been reached
- for (sequenceTriggerIndex in sequenceTriggers) {
- val timeoutTime = sequenceTriggersTimeoutTimes[sequenceTriggerIndex] ?: -1
- val trigger = triggers[sequenceTriggerIndex]
- val constraintState = triggerConstraints[sequenceTriggerIndex]
+ for (triggerIndex in sequenceTriggers) {
+ val timeoutTime = sequenceTriggersTimeoutTimes[triggerIndex] ?: -1
- if (constraintState.constraints.isNotEmpty()) {
- if (!constraintSnapshot.isSatisfied(constraintState)) continue
+ if (!triggersSatisfiedByConstraints.contains(triggerIndex)) {
+ continue
}
if (timeoutTime != -1L && currentTime >= timeoutTime) {
- lastMatchedEventIndices[sequenceTriggerIndex] = -1
- sequenceTriggersTimeoutTimes[sequenceTriggerIndex] = -1
+ lastMatchedEventIndices[triggerIndex] = -1
+ sequenceTriggersTimeoutTimes[triggerIndex] = -1
} else {
+ val triggerKeys = triggers[triggerIndex].keys
+
// consume the event if the trigger contains this keycode.
- trigger.keys.forEachIndexed { keyIndex, key ->
- if (key.keyCode == event.keyCode && trigger.keys[keyIndex].consumeKeyEvent) {
+ for ((keyIndex, key) in triggerKeys.withIndex()) {
+ if (key !is KeyCodeTriggerKey) {
+ continue
+ }
+
+ if (key.keyCode == event.keyCode && triggerKeys[keyIndex].consumeKeyEvent) {
consumeEvent = true
}
}
}
}
- doublePressTimeoutTimes.forEachIndexed { doublePressEventIndex, timeoutTime ->
+ for ((doublePressEventIndex, timeoutTime) in doublePressTimeoutTimes.withIndex()) {
if (currentTime >= timeoutTime) {
doublePressTimeoutTimes[doublePressEventIndex] = -1
doublePressEventStates[doublePressEventIndex] = NOT_PRESSED
} else {
val eventLocation = doublePressTriggerKeys[doublePressEventIndex]
+ val triggerIndex = eventLocation.triggerIndex
+
+ // Ignore this double press trigger if the constraint isn't satisfied.
+ if (!triggersSatisfiedByConstraints.contains(triggerIndex)) {
+ continue
+ }
+
val doublePressEvent =
triggers[eventLocation.triggerIndex].keys[eventLocation.keyIndex]
- val triggerIndex = eventLocation.triggerIndex
triggers[triggerIndex].keys.forEachIndexed { eventIndex, event ->
if (event == doublePressEvent &&
@@ -669,16 +706,13 @@ class KeyMapController(
Otherwise the order of the key maps affects the logic.
*/
triggerLoop@ for (triggerIndex in parallelTriggers) {
- val trigger = triggers[triggerIndex]
- val lastMatchedIndex = lastMatchedEventIndices[triggerIndex]
+ if (!triggersSatisfiedByConstraints.contains(triggerIndex)) {
+ continue
+ }
- val constraintState = triggerConstraints[triggerIndex]
+ val trigger = triggers[triggerIndex]
- if (constraintState.constraints.isNotEmpty()) {
- if (!constraintSnapshot.isSatisfied(constraintState)) {
- continue
- }
- }
+ val lastMatchedIndex = lastMatchedEventIndices[triggerIndex]
for (actionKey in triggerActions[triggerIndex]) {
if (canActionBePerformed[actionKey] == null) {
@@ -701,26 +735,22 @@ class KeyMapController(
val nextIndex = lastMatchedIndex + 1
- if (trigger.matchingEventAtIndex(
- event.withShortPress,
- nextIndex,
- )
- ) {
+ if (trigger.matchingEventAtIndex(event.withShortPress, nextIndex)) {
lastMatchedEventIndices[triggerIndex] = nextIndex
parallelTriggerEventsAwaitingRelease[triggerIndex][nextIndex] = true
}
- if (trigger.matchingEventAtIndex(
- event.withLongPress,
- nextIndex,
- )
- ) {
+ if (trigger.matchingEventAtIndex(event.withLongPress, nextIndex)) {
lastMatchedEventIndices[triggerIndex] = nextIndex
parallelTriggerEventsAwaitingRelease[triggerIndex][nextIndex] = true
}
}
triggerLoop@ for (triggerIndex in parallelTriggers) {
+ if (!triggersSatisfiedByConstraints.contains(triggerIndex)) {
+ continue
+ }
+
val trigger = triggers[triggerIndex]
val lastMatchedIndex = lastMatchedEventIndices[triggerIndex]
@@ -741,11 +771,7 @@ class KeyMapController(
}
// Perform short press action
- if (trigger.matchingEventAtIndex(
- event.withShortPress,
- lastMatchedIndex,
- )
- ) {
+ if (trigger.matchingEventAtIndex(event.withShortPress, lastMatchedIndex)) {
if (trigger.keys[lastMatchedIndex].consumeKeyEvent) {
consumeEvent = true
}
@@ -773,10 +799,7 @@ class KeyMapController(
detectedShortPressTriggers.add(triggerIndex)
val vibrateDuration = when {
- trigger.vibrate -> {
- vibrateDuration(trigger)
- }
-
+ trigger.vibrate -> vibrateDuration(trigger)
forceVibrate.value -> defaultVibrateDuration.value
else -> -1L
}
@@ -787,11 +810,7 @@ class KeyMapController(
}
// Perform long press action
- if (trigger.matchingEventAtIndex(
- event.withLongPress,
- lastMatchedIndex,
- )
- ) {
+ if (trigger.matchingEventAtIndex(event.withLongPress, lastMatchedIndex)) {
if (trigger.keys[lastMatchedIndex].consumeKeyEvent) {
consumeEvent = true
}
@@ -799,8 +818,7 @@ class KeyMapController(
if (lastMatchedIndex == trigger.keys.lastIndex) {
awaitingLongPress = true
- if (trigger.longPressDoubleVibration
- ) {
+ if (trigger.longPressDoubleVibration) {
useCase.vibrate(vibrateDuration(trigger))
}
@@ -836,8 +854,16 @@ class KeyMapController(
}
if (detectedShortPressTriggers.isNotEmpty()) {
- val matchingDoublePressEvent = doublePressTriggerKeys.any {
- triggers[it.triggerIndex].keys[it.keyIndex].matchesEvent(event.withDoublePress)
+ val matchingDoublePressEvent = doublePressTriggerKeys.any { keyLocation ->
+ // See issue #1271. Only consider the double press triggers that overlap
+ // if the constraints allow it.
+
+ if (!triggersSatisfiedByConstraints.contains(keyLocation.triggerIndex)) {
+ return@any false
+ }
+
+ val key = triggers[keyLocation.triggerIndex].keys[keyLocation.keyIndex]
+ key.matchesEvent(event.withDoublePress)
}
/* to prevent the actions of keys mapped to a short press and, a long press or a double press
@@ -852,16 +878,17 @@ class KeyMapController(
performActionsOnFailedLongPress.addAll(detectedShortPressTriggers)
}
- else -> detectedShortPressTriggers.forEach { triggerIndex ->
+ else -> {
+ for (triggerIndex in detectedShortPressTriggers) {
+ if (triggers[triggerIndex].showToast) {
+ showToast = true
+ }
- if (triggers[triggerIndex].showToast) {
- showToast = true
+ parallelTriggerActionPerformers[triggerIndex]?.onTriggered(
+ calledOnTriggerRelease = false,
+ metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions),
+ )
}
-
- parallelTriggerActionPerformers[triggerIndex]?.onTriggered(
- calledOnTriggerRelease = false,
- metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions),
- )
}
}
}
@@ -882,13 +909,14 @@ class KeyMapController(
return true
}
- sequenceTriggers.forEach { triggerIndex ->
- val trigger = triggers[triggerIndex]
- val constraints = triggerConstraints[triggerIndex]
+ for (triggerIndex in sequenceTriggers) {
+ if (!triggersSatisfiedByConstraints.contains(triggerIndex)) {
+ continue
+ }
- if (!constraintSnapshot.isSatisfied(constraints)) return@forEach
+ val trigger = triggers[triggerIndex]
- trigger.keys.forEachIndexed { keyIndex, key ->
+ for (key in trigger.keys) {
val matchingEvent = when {
key.matchesEvent(event.withShortPress) -> true
key.matchesEvent(event.withLongPress) -> true
@@ -1015,7 +1043,7 @@ class KeyMapController(
// the index of the next event to match in the trigger
val nextIndex = lastMatchedEventIndex + 1
- if ((currentTime - downTime) >= longPressDelay(triggers[triggerIndex])) {
+ if ((currentTime - downTime) >= longPressDelay(trigger)) {
successfulLongPressTrigger = true
} else if (detectSequenceLongPresses &&
longPressSequenceTriggerKeys.any { it.matchesEvent(event.withLongPress) }
@@ -1036,12 +1064,8 @@ class KeyMapController(
}
// if the next event matches the event just pressed
- if (triggers[triggerIndex].matchingEventAtIndex(
- encodedEventWithClickType,
- nextIndex,
- )
- ) {
- if (triggers[triggerIndex].keys[nextIndex].consumeKeyEvent) {
+ if (trigger.matchingEventAtIndex(encodedEventWithClickType, nextIndex)) {
+ if (trigger.keys[nextIndex].consumeKeyEvent) {
consumeEvent = true
}
@@ -1053,7 +1077,7 @@ class KeyMapController(
*/
if (nextIndex == 0) {
val startTime = currentTime
- val timeout = sequenceTriggerTimeout(triggers[triggerIndex])
+ val timeout = sequenceTriggerTimeout(trigger)
sequenceTriggersTimeoutTimes[triggerIndex] = startTime + timeout
}
@@ -1062,16 +1086,16 @@ class KeyMapController(
If the last event in a trigger has been matched, then the action needs to be performed and the timer
reset.
*/
- if (nextIndex == triggers[triggerIndex].keys.lastIndex) {
+ if (nextIndex == trigger.keys.lastIndex) {
detectedSequenceTriggerIndexes.add(triggerIndex)
- if (triggers[triggerIndex].showToast) {
+ if (trigger.showToast) {
showToast = true
}
- triggerActions[triggerIndex].forEachIndexed { index, _ ->
- if (triggers[triggerIndex].vibrate) {
- vibrateDurations.add(vibrateDuration(triggers[triggerIndex]))
+ triggerActions[triggerIndex].forEach { _ ->
+ if (trigger.vibrate) {
+ vibrateDurations.add(vibrateDuration(trigger))
}
}
@@ -1461,7 +1485,7 @@ class KeyMapController(
}
}
- private fun KeyMapTrigger.matchingEventAtIndex(event: Event, index: Int): Boolean {
+ private fun Trigger.matchingEventAtIndex(event: Event, index: Int): Boolean {
if (index >= this.keys.size) return false
val key = this.keys[index]
@@ -1469,46 +1493,58 @@ class KeyMapController(
return key.matchesEvent(event)
}
- private fun TriggerKey.matchesEvent(event: Event): Boolean = when (this.device) {
- TriggerKeyDevice.Any -> this.keyCode == event.keyCode && this.clickType == event.clickType
- is TriggerKeyDevice.External ->
- this.keyCode == event.keyCode &&
- event.descriptor != null &&
- event.descriptor == this.device.descriptor &&
- this.clickType == event.clickType
-
- TriggerKeyDevice.Internal ->
- this.keyCode == event.keyCode &&
- event.descriptor == null &&
- this.clickType == event.clickType
+ private fun TriggerKey.matchesEvent(event: Event): Boolean {
+ if (this !is KeyCodeTriggerKey) {
+ return false
+ }
+
+ return when (this.device) {
+ TriggerKeyDevice.Any -> this.keyCode == event.keyCode && this.clickType == event.clickType
+ is TriggerKeyDevice.External ->
+ this.keyCode == event.keyCode &&
+ event.descriptor != null &&
+ event.descriptor == this.device.descriptor &&
+ this.clickType == event.clickType
+
+ TriggerKeyDevice.Internal ->
+ this.keyCode == event.keyCode &&
+ event.descriptor == null &&
+ this.clickType == event.clickType
+ }
}
- private fun TriggerKey.matchesWithOtherKey(otherKey: TriggerKey): Boolean = when (this.device) {
- TriggerKeyDevice.Any ->
- this.keyCode == otherKey.keyCode &&
- this.clickType == otherKey.clickType
+ private fun TriggerKey.matchesWithOtherKey(otherKey: TriggerKey): Boolean {
+ if (!(this is KeyCodeTriggerKey && otherKey is KeyCodeTriggerKey)) {
+ return false
+ }
+
+ return when (this.device) {
+ TriggerKeyDevice.Any ->
+ this.keyCode == otherKey.keyCode &&
+ this.clickType == otherKey.clickType
- is TriggerKeyDevice.External ->
- this.keyCode == otherKey.keyCode &&
- this.device == otherKey.device &&
- this.clickType == otherKey.clickType
+ is TriggerKeyDevice.External ->
+ this.keyCode == otherKey.keyCode &&
+ this.device == otherKey.device &&
+ this.clickType == otherKey.clickType
- TriggerKeyDevice.Internal ->
- this.keyCode == otherKey.keyCode &&
- otherKey.device == TriggerKeyDevice.Internal &&
- this.clickType == otherKey.clickType
+ TriggerKeyDevice.Internal ->
+ this.keyCode == otherKey.keyCode &&
+ otherKey.device == TriggerKeyDevice.Internal &&
+ this.clickType == otherKey.clickType
+ }
}
- private fun longPressDelay(trigger: KeyMapTrigger): Long =
+ private fun longPressDelay(trigger: Trigger): Long =
trigger.longPressDelay?.toLong() ?: defaultLongPressDelay.value
- private fun doublePressTimeout(trigger: KeyMapTrigger): Long =
+ private fun doublePressTimeout(trigger: Trigger): Long =
trigger.doublePressDelay?.toLong() ?: defaultDoublePressDelay.value
- private fun vibrateDuration(trigger: KeyMapTrigger): Long =
+ private fun vibrateDuration(trigger: Trigger): Long =
trigger.vibrateDuration?.toLong() ?: defaultVibrateDuration.value
- private fun sequenceTriggerTimeout(trigger: KeyMapTrigger): Long =
+ private fun sequenceTriggerTimeout(trigger: Trigger): Long =
trigger.sequenceTriggerTimeout?.toLong() ?: defaultSequenceTriggerTimeout.value
private fun setActionMapAndOptions(actions: Set) {
@@ -1561,4 +1597,11 @@ class KeyMapController(
)
private data class TriggerKeyLocation(val triggerIndex: Int, val keyIndex: Int)
+
+ private val TriggerKey.consumeKeyEvent: Boolean
+ get() = if (this is KeyCodeTriggerKey) {
+ this.consumeEvent
+ } else {
+ false
+ }
}
diff --git a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AdvancedTriggersBottomSheet.kt b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AdvancedTriggersBottomSheet.kt
new file mode 100644
index 0000000000..b0c16cb0de
--- /dev/null
+++ b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AdvancedTriggersBottomSheet.kt
@@ -0,0 +1,141 @@
+package io.github.sds100.keymapper.mappings.keymaps.trigger
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.SheetState
+import androidx.compose.material3.SheetValue.Expanded
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.github.sds100.keymapper.R
+import io.github.sds100.keymapper.compose.KeyMapperTheme
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AdvancedTriggersBottomSheet(
+ modifier: Modifier = Modifier,
+ onDismissRequest: () -> Unit,
+ viewModel: ConfigTriggerViewModel,
+ sheetState: SheetState,
+) {
+ AdvancedTriggersBottomSheet(
+ modifier,
+ onDismissRequest,
+ sheetState,
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AdvancedTriggersBottomSheet(
+ modifier: Modifier = Modifier,
+ onDismissRequest: () -> Unit,
+ sheetState: SheetState,
+) {
+ val scope = rememberCoroutineScope()
+
+ ModalBottomSheet(
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ sheetState = sheetState,
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ text = stringResource(R.string.advanced_triggers_sheet_title),
+ style = MaterialTheme.typography.headlineMedium,
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ text = stringResource(R.string.advanced_triggers_sheet_text),
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ text = stringResource(R.string.purchasing_not_implemented_bottom_sheet_text),
+ fontStyle = FontStyle.Italic,
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ val uriHandler = LocalUriHandler.current
+ val googlePlayUrl = stringResource(R.string.url_play_store_listing)
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ OutlinedButton(
+ modifier = Modifier,
+ onClick = {
+ scope.launch {
+ sheetState.hide()
+ onDismissRequest()
+ }
+ },
+ ) {
+ Text(stringResource(R.string.neg_cancel))
+ }
+
+ FilledTonalButton(
+ modifier = Modifier,
+ onClick = {
+ scope.launch {
+ uriHandler.openUri(googlePlayUrl)
+ }
+ },
+ ) {
+ Text(stringResource(R.string.purchasing_download_key_mapper_from_google_play))
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+private fun Preview() {
+ KeyMapperTheme {
+ val sheetState = SheetState(
+ skipPartiallyExpanded = true,
+ density = LocalDensity.current,
+ initialValue = Expanded,
+ )
+
+ AdvancedTriggersBottomSheet(
+ onDismissRequest = {},
+ sheetState = sheetState,
+ )
+ }
+}
diff --git a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt
new file mode 100644
index 0000000000..8f46a6afcd
--- /dev/null
+++ b/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt
@@ -0,0 +1,28 @@
+package io.github.sds100.keymapper.mappings.keymaps.trigger
+
+import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCase
+import io.github.sds100.keymapper.mappings.keymaps.CreateKeyMapShortcutUseCase
+import io.github.sds100.keymapper.mappings.keymaps.DisplayKeyMapUseCase
+import io.github.sds100.keymapper.onboarding.OnboardingUseCase
+import io.github.sds100.keymapper.purchasing.PurchasingManager
+import io.github.sds100.keymapper.util.ui.ResourceProvider
+import kotlinx.coroutines.CoroutineScope
+
+class ConfigTriggerViewModel(
+ coroutineScope: CoroutineScope,
+ onboarding: OnboardingUseCase,
+ config: ConfigKeyMapUseCase,
+ recordTrigger: RecordTriggerUseCase,
+ createKeyMapShortcut: CreateKeyMapShortcutUseCase,
+ displayKeyMap: DisplayKeyMapUseCase,
+ resourceProvider: ResourceProvider,
+ private val purchasingManager: PurchasingManager,
+) : BaseConfigTriggerViewModel(
+ coroutineScope,
+ onboarding,
+ config,
+ recordTrigger,
+ createKeyMapShortcut,
+ displayKeyMap,
+ resourceProvider,
+)
diff --git a/app/src/free/java/io/github/sds100/keymapper/purchasing/PurchasingManagerImpl.kt b/app/src/free/java/io/github/sds100/keymapper/purchasing/PurchasingManagerImpl.kt
new file mode 100644
index 0000000000..b1caee03a0
--- /dev/null
+++ b/app/src/free/java/io/github/sds100/keymapper/purchasing/PurchasingManagerImpl.kt
@@ -0,0 +1,26 @@
+package io.github.sds100.keymapper.purchasing
+
+import android.content.Context
+import io.github.sds100.keymapper.util.Error
+import io.github.sds100.keymapper.util.Result
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+class PurchasingManagerImpl(
+ context: Context,
+ private val coroutineScope: CoroutineScope,
+) : PurchasingManager {
+ override val onCompleteProductPurchase: MutableSharedFlow = MutableSharedFlow()
+
+ override suspend fun launchPurchasingFlow(product: ProductId): Result {
+ return Error.PurchasingNotImplemented
+ }
+
+ override suspend fun getProductPrice(product: ProductId): Result {
+ return Error.PurchasingNotImplemented
+ }
+
+ override suspend fun isPurchased(product: ProductId): Result {
+ return Error.PurchasingNotImplemented
+ }
+}
diff --git a/app/src/free/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/free/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt
new file mode 100644
index 0000000000..9e7bfe2a1b
--- /dev/null
+++ b/app/src/free/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt
@@ -0,0 +1,48 @@
+package io.github.sds100.keymapper.system.accessibility
+
+import io.github.sds100.keymapper.actions.PerformActionsUseCase
+import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase
+import io.github.sds100.keymapper.data.repositories.PreferenceRepository
+import io.github.sds100.keymapper.mappings.PauseMappingsUseCase
+import io.github.sds100.keymapper.mappings.fingerprintmaps.DetectFingerprintMapsUseCase
+import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase
+import io.github.sds100.keymapper.reroutekeyevents.RerouteKeyEventsUseCase
+import io.github.sds100.keymapper.system.devices.DevicesAdapter
+import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter
+import io.github.sds100.keymapper.system.root.SuAdapter
+import io.github.sds100.keymapper.util.ServiceEvent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+
+class AccessibilityServiceController(
+ coroutineScope: CoroutineScope,
+ accessibilityService: IAccessibilityService,
+ inputEvents: SharedFlow,
+ outputEvents: MutableSharedFlow,
+ detectConstraintsUseCase: DetectConstraintsUseCase,
+ performActionsUseCase: PerformActionsUseCase,
+ detectKeyMapsUseCase: DetectKeyMapsUseCase,
+ detectFingerprintMapsUseCase: DetectFingerprintMapsUseCase,
+ rerouteKeyEventsUseCase: RerouteKeyEventsUseCase,
+ pauseMappingsUseCase: PauseMappingsUseCase,
+ devicesAdapter: DevicesAdapter,
+ suAdapter: SuAdapter,
+ inputMethodAdapter: InputMethodAdapter,
+ settingsRepository: PreferenceRepository,
+) : BaseAccessibilityServiceController(
+ coroutineScope,
+ accessibilityService,
+ inputEvents,
+ outputEvents,
+ detectConstraintsUseCase,
+ performActionsUseCase,
+ detectKeyMapsUseCase,
+ detectFingerprintMapsUseCase,
+ rerouteKeyEventsUseCase,
+ pauseMappingsUseCase,
+ devicesAdapter,
+ suAdapter,
+ inputMethodAdapter,
+ settingsRepository,
+)
diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt
index cce097e7c6..1a48756871 100644
--- a/app/src/main/assets/whats-new.txt
+++ b/app/src/main/assets/whats-new.txt
@@ -1 +1,3 @@
-Support for Android 14 and many bug fixes. See the changelog.
\ No newline at end of file
+New trigger 🎉!
+
+You can now trigger your key maps from any of the ways your phone launches the assistant! This could be the Bixby button, Power button, or a button on your headset.
\ No newline at end of file
diff --git a/app/src/main/java/io/github/sds100/keymapper/ActivityViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/ActivityViewModel.kt
index 0de03bff3b..8fefdddbe4 100644
--- a/app/src/main/java/io/github/sds100/keymapper/ActivityViewModel.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/ActivityViewModel.kt
@@ -21,6 +21,7 @@ class ActivityViewModel(
PopupViewModel by PopupViewModelImpl(),
NavigationViewModel by NavigationViewModelImpl() {
+ var handledActivityLaunchIntent: Boolean = false
var previousNightMode: Int? = null
fun onCantFindAccessibilitySettings() {
diff --git a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt b/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt
similarity index 67%
rename from app/src/main/java/io/github/sds100/keymapper/MainActivity.kt
rename to app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt
index 4e3580d10a..efc0b158db 100644
--- a/app/src/main/java/io/github/sds100/keymapper/MainActivity.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt
@@ -12,6 +12,7 @@ import androidx.navigation.findNavController
import io.github.sds100.keymapper.Constants.PACKAGE_NAME
import io.github.sds100.keymapper.databinding.ActivityMainBinding
import io.github.sds100.keymapper.system.permissions.RequestPermissionDelegate
+import io.github.sds100.keymapper.util.launchRepeatOnLifecycle
import io.github.sds100.keymapper.util.ui.showPopups
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -21,11 +22,14 @@ import timber.log.Timber
* Created by sds100 on 19/02/2020.
*/
-class MainActivity : AppCompatActivity() {
+abstract class BaseMainActivity : AppCompatActivity() {
companion object {
const val ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG =
"$PACKAGE_NAME.ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG"
+
+ const val ACTION_USE_ASSISTANT_TRIGGER =
+ "$PACKAGE_NAME.ACTION_USE_ASSISTANT_TRIGGER"
}
private val viewModel by viewModels {
@@ -51,7 +55,7 @@ class MainActivity : AppCompatActivity() {
requestPermissionDelegate = RequestPermissionDelegate(this, showDialogs = true)
- ServiceLocator.permissionAdapter(this@MainActivity).request
+ ServiceLocator.permissionAdapter(this@BaseMainActivity).request
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { permission ->
requestPermissionDelegate.requestPermission(
@@ -61,8 +65,29 @@ class MainActivity : AppCompatActivity() {
}
.launchIn(lifecycleScope)
- if (intent.action == ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG) {
- viewModel.onCantFindAccessibilitySettings()
+ // Must launch when the activity is resumed
+ // so the nav controller can be found
+ launchRepeatOnLifecycle(Lifecycle.State.RESUMED) {
+ if (viewModel.handledActivityLaunchIntent) {
+ return@launchRepeatOnLifecycle
+ }
+
+ when (intent.action) {
+ ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG -> {
+ viewModel.onCantFindAccessibilitySettings()
+ }
+
+ ACTION_USE_ASSISTANT_TRIGGER -> {
+ findNavController(R.id.container).navigate(
+ NavAppDirections.actionToConfigKeymap(
+ keymapUid = null,
+ showAdvancedTriggers = true,
+ ),
+ )
+ }
+ }
+
+ viewModel.handledActivityLaunchIntent = true
}
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt b/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt
index f864d19539..c7824b42f0 100644
--- a/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt
@@ -13,6 +13,7 @@ import io.github.sds100.keymapper.data.Keys
import io.github.sds100.keymapper.data.entities.LogEntryEntity
import io.github.sds100.keymapper.logging.KeyMapperLoggingTree
import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerController
+import io.github.sds100.keymapper.purchasing.PurchasingManagerImpl
import io.github.sds100.keymapper.settings.ThemeUtils
import io.github.sds100.keymapper.shizuku.ShizukuAdapterImpl
import io.github.sds100.keymapper.system.AndroidSystemFeatureAdapter
@@ -108,6 +109,7 @@ class KeyMapperApp : MultiDexApplication() {
suAdapter,
notificationReceiverAdapter,
ServiceLocator.settingsRepository(this),
+ packageManagerAdapter,
)
}
@@ -118,7 +120,7 @@ class KeyMapperApp : MultiDexApplication() {
val fileAdapter by lazy { AndroidFileAdapter(this) }
val popupMessageAdapter by lazy { AndroidToastAdapter(this) }
val vibratorAdapter by lazy { AndroidVibratorAdapter(this) }
- val displayAdapter by lazy { AndroidDisplayAdapter(this) }
+ val displayAdapter by lazy { AndroidDisplayAdapter(this, coroutineScope = appCoroutineScope) }
val audioAdapter by lazy { AndroidVolumeAdapter(this) }
val suAdapter by lazy {
SuAdapterImpl(
@@ -152,6 +154,10 @@ class KeyMapperApp : MultiDexApplication() {
)
}
+ val purchasingManager: PurchasingManagerImpl by lazy {
+ PurchasingManagerImpl(this.applicationContext, appCoroutineScope)
+ }
+
private val loggingTree by lazy {
KeyMapperLoggingTree(
appCoroutineScope,
@@ -211,13 +217,13 @@ class KeyMapperApp : MultiDexApplication() {
ServiceLocator.settingsRepository(this),
notificationAdapter,
suAdapter,
+ permissionAdapter,
),
UseCases.pauseMappings(this),
UseCases.showImePicker(this),
UseCases.controlAccessibilityService(this),
UseCases.toggleCompatibleIme(this),
ShowHideInputMethodUseCaseImpl(ServiceLocator.accessibilityServiceAdapter(this)),
- UseCases.fingerprintGesturesSupported(this),
UseCases.onboarding(this),
ServiceLocator.resourceProvider(this),
)
diff --git a/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt b/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt
index e2df1d2baa..01286c4f87 100755
--- a/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt
@@ -15,6 +15,7 @@ import io.github.sds100.keymapper.data.repositories.RoomLogRepository
import io.github.sds100.keymapper.data.repositories.SettingsPreferenceRepository
import io.github.sds100.keymapper.logging.LogRepository
import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintMapRepository
+import io.github.sds100.keymapper.purchasing.PurchasingManagerImpl
import io.github.sds100.keymapper.shizuku.ShizukuAdapter
import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter
import io.github.sds100.keymapper.system.airplanemode.AirplaneModeAdapter
@@ -24,7 +25,7 @@ import io.github.sds100.keymapper.system.bluetooth.BluetoothAdapter
import io.github.sds100.keymapper.system.camera.CameraAdapter
import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter
import io.github.sds100.keymapper.system.devices.DevicesAdapter
-import io.github.sds100.keymapper.system.display.DisplayAdapter
+import io.github.sds100.keymapper.system.display.AndroidDisplayAdapter
import io.github.sds100.keymapper.system.files.FileAdapter
import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter
import io.github.sds100.keymapper.system.intents.IntentAdapter
@@ -70,7 +71,6 @@ object ServiceLocator {
synchronized(this) {
return roomKeymapRepository ?: RoomKeyMapRepository(
database(context).keymapDao(),
- devicesAdapter(context),
(context.applicationContext as KeyMapperApp).appCoroutineScope,
).also {
this.roomKeymapRepository = it
@@ -205,7 +205,7 @@ object ServiceLocator {
fun vibratorAdapter(context: Context): VibratorAdapter =
(context.applicationContext as KeyMapperApp).vibratorAdapter
- fun displayAdapter(context: Context): DisplayAdapter =
+ fun displayAdapter(context: Context): AndroidDisplayAdapter =
(context.applicationContext as KeyMapperApp).displayAdapter
fun audioAdapter(context: Context): VolumeAdapter =
@@ -253,6 +253,9 @@ object ServiceLocator {
fun appCoroutineScope(context: Context): CoroutineScope =
(context.applicationContext as KeyMapperApp).appCoroutineScope
+ fun purchasingManager(context: Context): PurchasingManagerImpl =
+ (context.applicationContext as KeyMapperApp).purchasingManager
+
private fun createDatabase(context: Context): AppDatabase = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
diff --git a/app/src/main/java/io/github/sds100/keymapper/SplashActivity.kt b/app/src/main/java/io/github/sds100/keymapper/SplashActivity.kt
index db52c9132b..43cb93b643 100644
--- a/app/src/main/java/io/github/sds100/keymapper/SplashActivity.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/SplashActivity.kt
@@ -31,17 +31,6 @@ class SplashActivity : FragmentActivity() {
// Otherwise, show the slides when they are setting up the app for the first time.
if (onboarding.shownAppIntro) {
appIntroSlides = sequence {
- if (!onboarding.approvedFingerprintFeaturePrompt &&
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
- systemFeatureAdapter.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
- ) {
- yield(AppIntroSlide.FINGERPRINT_GESTURE_SUPPORT)
- }
-
- if (onboarding.showSetupChosenDevicesAgainAppIntro.firstBlocking()) {
- yield(AppIntroSlide.SETUP_CHOSEN_DEVICES_AGAIN)
- }
-
if (onboarding.promptForShizukuPermission.firstBlocking()) {
yield(AppIntroSlide.GRANT_SHIZUKU_PERMISSION)
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt
index 66c73eb798..7950a5dad0 100644
--- a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt
@@ -50,6 +50,7 @@ object UseCases {
ServiceLocator.inputMethodAdapter(ctx),
displaySimpleMapping(ctx),
ServiceLocator.settingsRepository(ctx),
+ ServiceLocator.purchasingManager(ctx),
)
fun configKeyMap(ctx: Context): ConfigKeyMapUseCase = ConfigKeyMapUseCaseImpl(
@@ -121,6 +122,7 @@ object UseCases {
fun controlAccessibilityService(ctx: Context): ControlAccessibilityServiceUseCase =
ControlAccessibilityServiceUseCaseImpl(
ServiceLocator.accessibilityServiceAdapter(ctx),
+ ServiceLocator.permissionAdapter(ctx),
)
fun toggleCompatibleIme(ctx: Context) =
diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt
index 268018c5f0..cb5271068a 100644
--- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt
@@ -26,6 +26,8 @@ import io.github.sds100.keymapper.data.entities.ConstraintEntity
import io.github.sds100.keymapper.data.entities.Extra
import io.github.sds100.keymapper.data.entities.FingerprintMapEntity
import io.github.sds100.keymapper.data.entities.KeyMapEntity
+import io.github.sds100.keymapper.data.entities.TriggerEntity
+import io.github.sds100.keymapper.data.entities.TriggerKeyEntity
import io.github.sds100.keymapper.data.migration.JsonMigration
import io.github.sds100.keymapper.data.migration.Migration10To11
import io.github.sds100.keymapper.data.migration.Migration11To12
@@ -110,6 +112,8 @@ class BackupManagerImpl(
GsonBuilder()
.registerTypeAdapter(FingerprintMapEntity.DESERIALIZER)
.registerTypeAdapter(KeyMapEntity.DESERIALIZER)
+ .registerTypeAdapter(TriggerEntity.DESERIALIZER)
+ .registerTypeAdapter(TriggerKeyEntity.DESERIALIZER)
.registerTypeAdapter(ActionEntity.DESERIALIZER)
.registerTypeAdapter(Extra.DESERIALIZER)
.registerTypeAdapter(ConstraintEntity.DESERIALIZER).create()
@@ -262,7 +266,6 @@ class BackupManagerImpl(
private suspend fun restore(inputStream: InputStream, soundFiles: List): Result<*> {
try {
val parser = JsonParser()
- val gson = Gson()
val rootElement = inputStream.bufferedReader().use {
val element = parser.parse(it)
@@ -299,10 +302,7 @@ class BackupManagerImpl(
Migration11To12.migrateKeyMap(json, deviceInfoList ?: JsonArray())
},
// do nothing because this added the log table
- JsonMigration(
- 12,
- 13,
- ) { json -> json },
+ JsonMigration(12, 13) { json -> json },
)
keymapListJsonArray?.forEach { keyMap ->
@@ -330,10 +330,7 @@ class BackupManagerImpl(
// do nothing because this added the log table
val newFingerprintMapMigrations = listOf(
- JsonMigration(
- 12,
- 13,
- ) { json -> json },
+ JsonMigration(12, 13) { json -> json },
)
if (rootElement.contains(NAME_FINGERPRINT_MAP_LIST) && backupDbVersion >= 12) {
@@ -429,8 +426,9 @@ class BackupManagerImpl(
} catch (e: NoSuchElementException) {
return Error.CorruptJsonFile(e.message ?: "")
} catch (e: Exception) {
+ e.printStackTrace()
+
if (throwExceptions) {
- e.printStackTrace()
throw e
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt
new file mode 100644
index 0000000000..d4b067d4b5
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt
@@ -0,0 +1,64 @@
+package io.github.sds100.keymapper.compose
+
+import androidx.compose.ui.graphics.Color
+
+object ComposeColors {
+ val primaryLight = Color(0xFF175DB2)
+ val onPrimaryLight = Color(0xFFFFFFFF)
+ val primaryContainerLight = Color(0xFFD6E3FF)
+ val onPrimaryContainerLight = Color(0xFF001B3F)
+ val secondaryLight = Color(0xFF005EB5)
+ val onSecondaryLight = Color(0xFFFFFFFF)
+ val secondaryContainerLight = Color(0xFF001B3D)
+ val onSecondaryContainerLight = Color(0xFFD4E3FF)
+ val tertiaryLight = Color(0xFF0061A3)
+ val onTertiaryLight = Color(0xFFFFFFFF)
+ val tertiaryContainerLight = Color(0xFFCFE4FF)
+ val onTertiaryContainerLight = Color(0xFF001D36)
+ val errorLight = Color(0xFFBA1B1B)
+ val onErrorLight = Color(0xFFFFFFFF)
+ val errorContainerLight = Color(0xFFFFDAD4)
+ val onErrorContainerLight = Color(0xFF410001)
+ val backgroundLight = Color(0xFFFDFBFF)
+ val onBackgroundLight = Color(0xFF1A1B1F)
+ val surfaceLight = Color(0xFFFDFBFF)
+ val onSurfaceLight = Color(0xFF1A1B1F)
+ val surfaceVariantLight = Color(0xFFE0E2EC)
+ val onSurfaceVariantLight = Color(0xFF44474F)
+ val outlineLight = Color(0xFF74777F)
+ val outlineVariantLight = Color(0xFFBFC8CA)
+ val inverseSurfaceLight = Color(0xFF2F3034)
+ val inverseOnSurfaceLight = Color(0xFFF2F0F4)
+ val inversePrimaryLight = Color(0xFFA8C7FF)
+ val redLight = Color(0xffd32f2f)
+ val onRedLight = Color(0xFFFFFFFF)
+
+ val primaryDark = Color(0xFFA8C7FF)
+ val onPrimaryDark = Color(0xFF002F66)
+ val primaryContainerDark = Color(0xFF004590)
+ val onPrimaryContainerDark = Color(0xFFD6E3FF)
+ val secondaryDark = Color(0xFFB1CBD0)
+ val onSecondaryDark = Color(0xFF003063)
+ val secondaryContainerDark = Color(0xFF00468A)
+ val onSecondaryContainerDark = Color(0xFFD4E3FF)
+ val tertiaryDark = Color(0xFF9BCAFF)
+ val onTertiaryDark = Color(0xFF003259)
+ val tertiaryContainerDark = Color(0xFF00497E)
+ val onTertiaryContainerDark = Color(0xFFCFE4FF)
+ val errorDark = Color(0xFFFFB4A9)
+ val onErrorDark = Color(0xFF930006)
+ val errorContainerDark = Color(0xFF93000A)
+ val onErrorContainerDark = Color(0xFFFFDAD4)
+ val backgroundDark = Color(0xFF1A1B1F)
+ val onBackgroundDark = Color(0xFFE3E2E6)
+ val surfaceDark = Color(0xFF1A1B1F)
+ val onSurfaceDark = Color(0xFFE3E2E6)
+ val surfaceVariantDark = Color(0xFF44474F)
+ val onSurfaceVariantDark = Color(0xFFC4C6CF)
+ val outlineDark = Color(0xFF8D9099)
+ val outlineVariantDark = Color(0xFF3F484A)
+ val inverseSurfaceDark = Color(0xFFE3E2E6)
+ val inverseOnSurfaceDark = Color(0xFF1A1B1F)
+ val redDark = Color(0xffff7961)
+ val onRedDark = Color(0xFFFFFFFF)
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt
new file mode 100644
index 0000000000..55545a9a3d
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt
@@ -0,0 +1,28 @@
+package io.github.sds100.keymapper.compose
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Stores the custom colors in a palette that changes
+ * depending on the light/dark theme. A CompositionLocalProvider
+ * is used in the KeyMapperTheme to provide the correct palette in a similar
+ * way to how MaterialTheme.current works.
+ */
+@Immutable
+data class ComposeCustomColors(
+ val red: Color = Color.Unspecified,
+ val onRed: Color = Color.Unspecified,
+) {
+ companion object {
+ val LightPalette = ComposeCustomColors(
+ red = ComposeColors.redLight,
+ onRed = ComposeColors.onRedLight,
+ )
+
+ val DarkPalette = ComposeCustomColors(
+ red = ComposeColors.redDark,
+ onRed = ComposeColors.onRedDark,
+ )
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeTheme.kt b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeTheme.kt
new file mode 100755
index 0000000000..380dabf5ad
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeTheme.kt
@@ -0,0 +1,119 @@
+package io.github.sds100.keymapper.compose
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Typography
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+object ComposeTheme {
+ val lightScheme = lightColorScheme(
+ primary = ComposeColors.primaryLight,
+ onPrimary = ComposeColors.onPrimaryLight,
+ primaryContainer = ComposeColors.primaryContainerLight,
+ onPrimaryContainer = ComposeColors.onPrimaryContainerLight,
+ secondary = ComposeColors.secondaryLight,
+ onSecondary = ComposeColors.onSecondaryLight,
+ secondaryContainer = ComposeColors.secondaryContainerLight,
+ onSecondaryContainer = ComposeColors.onSecondaryContainerLight,
+ tertiary = ComposeColors.tertiaryLight,
+ onTertiary = ComposeColors.onTertiaryLight,
+ tertiaryContainer = ComposeColors.tertiaryContainerLight,
+ onTertiaryContainer = ComposeColors.onTertiaryContainerLight,
+ error = ComposeColors.errorLight,
+ onError = ComposeColors.onErrorLight,
+ errorContainer = ComposeColors.errorContainerLight,
+ onErrorContainer = ComposeColors.onErrorContainerLight,
+ background = ComposeColors.backgroundLight,
+ onBackground = ComposeColors.onBackgroundLight,
+ surface = ComposeColors.surfaceLight,
+ onSurface = ComposeColors.onSurfaceLight,
+ surfaceVariant = ComposeColors.surfaceVariantLight,
+ onSurfaceVariant = ComposeColors.onSurfaceVariantLight,
+ outline = ComposeColors.outlineLight,
+ outlineVariant = ComposeColors.outlineVariantLight,
+ inverseSurface = ComposeColors.inverseSurfaceLight,
+ inverseOnSurface = ComposeColors.inverseOnSurfaceLight,
+ inversePrimary = ComposeColors.inversePrimaryLight,
+ )
+
+ val darkScheme =
+ darkColorScheme(
+ primary = ComposeColors.primaryDark,
+ onPrimary = ComposeColors.onPrimaryDark,
+ primaryContainer = ComposeColors.primaryContainerDark,
+ onPrimaryContainer = ComposeColors.onPrimaryContainerDark,
+ secondary = ComposeColors.secondaryDark,
+ onSecondary = ComposeColors.onSecondaryDark,
+ secondaryContainer = ComposeColors.secondaryContainerDark,
+ onSecondaryContainer = ComposeColors.onSecondaryContainerDark,
+ tertiary = ComposeColors.tertiaryDark,
+ onTertiary = ComposeColors.onTertiaryDark,
+ tertiaryContainer = ComposeColors.tertiaryContainerDark,
+ onTertiaryContainer = ComposeColors.onTertiaryContainerDark,
+ error = ComposeColors.errorDark,
+ onError = ComposeColors.onErrorDark,
+ errorContainer = ComposeColors.errorContainerDark,
+ onErrorContainer = ComposeColors.onErrorContainerDark,
+ background = ComposeColors.backgroundDark,
+ onBackground = ComposeColors.onBackgroundDark,
+ surface = ComposeColors.surfaceDark,
+ onSurface = ComposeColors.onSurfaceDark,
+ surfaceVariant = ComposeColors.surfaceVariantDark,
+ onSurfaceVariant = ComposeColors.onSurfaceVariantDark,
+ outline = ComposeColors.outlineDark,
+ outlineVariant = ComposeColors.outlineVariantDark,
+ inverseSurface = ComposeColors.inverseSurfaceDark,
+ inverseOnSurface = ComposeColors.inverseOnSurfaceDark,
+ )
+}
+
+val LocalCustomColorsPalette = staticCompositionLocalOf { ComposeCustomColors() }
+
+@Composable
+fun KeyMapperTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ val colorScheme = when {
+ dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
+ dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
+ darkTheme -> ComposeTheme.darkScheme
+ else -> ComposeTheme.lightScheme
+ }
+
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.surfaceContainer.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
+ }
+ }
+
+ val customColorsPalette =
+ if (darkTheme) ComposeCustomColors.DarkPalette else ComposeCustomColors.LightPalette
+
+ CompositionLocalProvider(
+ LocalCustomColorsPalette provides customColorsPalette,
+ ) {
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography(),
+ content = content,
+ )
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintSnapshot.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintSnapshot.kt
index 2bfb975d49..b136dec7b2 100644
--- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintSnapshot.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintSnapshot.kt
@@ -55,18 +55,7 @@ class ConstraintSnapshotImpl(
}
}
- override fun isSatisfied(constraintState: ConstraintState): Boolean =
- when (constraintState.mode) {
- ConstraintMode.AND -> {
- constraintState.constraints.all { isSatisfied(it) }
- }
-
- ConstraintMode.OR -> {
- constraintState.constraints.any { isSatisfied(it) }
- }
- }
-
- private fun isSatisfied(constraint: Constraint): Boolean {
+ override fun isSatisfied(constraint: Constraint): Boolean {
val isSatisfied = when (constraint) {
is Constraint.AppInForeground -> appInForeground == constraint.packageName
is Constraint.AppNotInForeground -> appInForeground != constraint.packageName
@@ -98,7 +87,6 @@ class ConstraintSnapshotImpl(
is Constraint.FlashlightOff -> !cameraAdapter.isFlashlightOn(constraint.lens)
is Constraint.FlashlightOn -> cameraAdapter.isFlashlightOn(constraint.lens)
is Constraint.WifiConnected -> {
- Timber.d("Connected WiFi ssid = $connectedWifiSSID")
if (constraint.ssid == null) {
// connected to any network
connectedWifiSSID != null
@@ -139,5 +127,22 @@ class ConstraintSnapshotImpl(
}
interface ConstraintSnapshot {
- fun isSatisfied(constraintState: ConstraintState): Boolean
+ fun isSatisfied(constraint: Constraint): Boolean
+}
+
+fun ConstraintSnapshot.isSatisfied(constraintState: ConstraintState): Boolean {
+ // Required in case OR is used with empty list of constraints.
+ if (constraintState.constraints.isEmpty()) {
+ return true
+ }
+
+ return when (constraintState.mode) {
+ ConstraintMode.AND -> {
+ constraintState.constraints.all { isSatisfied(it) }
+ }
+
+ ConstraintMode.OR -> {
+ constraintState.constraints.any { isSatisfied(it) }
+ }
+ }
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/GetConstraintErrorUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/GetConstraintErrorUseCase.kt
index c0713cde87..09092ba5ae 100644
--- a/app/src/main/java/io/github/sds100/keymapper/constraints/GetConstraintErrorUseCase.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/constraints/GetConstraintErrorUseCase.kt
@@ -71,14 +71,6 @@ class GetConstraintErrorUseCaseImpl(
return Error.PermissionDenied(Permission.WRITE_SETTINGS)
}
- Constraint.ScreenOff,
- Constraint.ScreenOn,
- -> {
- if (!permissionAdapter.isGranted(Permission.ROOT)) {
- return Error.PermissionDenied(Permission.ROOT)
- }
- }
-
is Constraint.FlashlightOn, is Constraint.FlashlightOff -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return Error.SdkVersionTooLow(minSdk = Build.VERSION_CODES.M)
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/app/src/main/java/io/github/sds100/keymapper/data/Keys.kt
index 1003328f38..9bed05c66f 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/Keys.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/Keys.kt
@@ -42,8 +42,9 @@ object Keys {
val hideHomeScreenAlerts = booleanPreferencesKey("pref_hide_home_screen_alerts")
val acknowledgedGuiKeyboard = booleanPreferencesKey("pref_acknowledged_gui_keyboard")
val showDeviceDescriptors = booleanPreferencesKey("pref_show_device_descriptors")
- val approvedFingerprintFeaturePrompt =
- booleanPreferencesKey("pref_approved_fingerprint_feature_prompt")
+
+ val approvedAssistantTriggerFeaturePrompt =
+ booleanPreferencesKey("pref_approved_assistant_trigger_feature_prompt")
val shownParallelTriggerOrderExplanation =
booleanPreferencesKey("key_shown_parallel_trigger_order_warning")
val shownSequenceTriggerExplanation =
@@ -58,9 +59,6 @@ object Keys {
val fingerprintGesturesAvailable =
booleanPreferencesKey("fingerprint_gestures_available")
- val approvedSetupChosenDevicesAgain =
- booleanPreferencesKey("pref_approved_new_choose_devices_settings")
-
val rerouteKeyEvents = booleanPreferencesKey("key_reroute_key_events_from_specified_devices")
val devicesToRerouteKeyEvents =
stringSetPreferencesKey("key_devices_to_reroute_key_events")
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/SeedDatabaseWorker.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/SeedDatabaseWorker.kt
deleted file mode 100644
index 3fba3334c5..0000000000
--- a/app/src/main/java/io/github/sds100/keymapper/data/db/SeedDatabaseWorker.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package io.github.sds100.keymapper.data.db
-
-import android.content.Context
-import android.view.KeyEvent
-import androidx.work.CoroutineWorker
-import androidx.work.WorkerParameters
-import io.github.sds100.keymapper.Constants
-import io.github.sds100.keymapper.ServiceLocator
-import io.github.sds100.keymapper.data.entities.ActionEntity
-import io.github.sds100.keymapper.data.entities.KeyMapEntity
-import io.github.sds100.keymapper.data.entities.TriggerEntity
-import kotlinx.coroutines.coroutineScope
-
-/**
- * Created by sds100 on 26/01/2020.
- */
-
-class SeedDatabaseWorker(
- context: Context,
- workerParams: WorkerParameters,
-) : CoroutineWorker(context, workerParams) {
- override suspend fun doWork(): Result = coroutineScope {
- try {
- val keymaps = sequence {
- for (i in 1..100) {
- yield(
- KeyMapEntity(
- id = 0,
- trigger = createRandomTrigger(),
- actionList = createRandomActionList(),
- flags = 0,
- ),
- )
- }
- }.toList().toTypedArray()
-
- ServiceLocator.roomKeymapRepository(applicationContext).insert(*keymaps)
-
- Result.success()
- } catch (e: Exception) {
- Result.failure()
- }
- }
-
- private fun createRandomTrigger(): TriggerEntity {
- val keys = sequence {
- yield(
- TriggerEntity.KeyEntity(
- KeyEvent.KEYCODE_CTRL_LEFT,
- TriggerEntity.KeyEntity.DEVICE_ID_THIS_DEVICE,
- null,
- TriggerEntity.SHORT_PRESS,
- ),
- )
- yield(
- TriggerEntity.KeyEntity(
- KeyEvent.KEYCODE_ALT_LEFT,
- TriggerEntity.KeyEntity.DEVICE_ID_ANY_DEVICE,
- null,
- TriggerEntity.LONG_PRESS,
- ),
- )
- yield(
- TriggerEntity.KeyEntity(
- KeyEvent.KEYCODE_DEL,
- TriggerEntity.KeyEntity.DEVICE_ID_THIS_DEVICE,
- null,
- TriggerEntity.SHORT_PRESS,
- ),
- )
- }.toList()
-
- return TriggerEntity(
- keys,
- mode = TriggerEntity.SEQUENCE,
- flags = TriggerEntity.TRIGGER_FLAG_VIBRATE,
- )
- }
-
- private fun createRandomActionList(): List = sequence {
- yield(
- ActionEntity(
- type = ActionEntity.Type.APP,
- data = Constants.PACKAGE_NAME,
- ),
- )
- yield(
- ActionEntity(
- type = ActionEntity.Type.APP,
- data = "this.app.doesnt.exist",
- ),
- )
- }.toList()
-}
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/TriggerTypeConverter.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/TriggerTypeConverter.kt
index 497e31718a..392e2d9015 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/TriggerTypeConverter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/TriggerTypeConverter.kt
@@ -7,6 +7,7 @@ import com.google.gson.Gson
import com.google.gson.GsonBuilder
import io.github.sds100.keymapper.data.entities.Extra
import io.github.sds100.keymapper.data.entities.TriggerEntity
+import io.github.sds100.keymapper.data.entities.TriggerKeyEntity
/**
* Created by sds100 on 05/09/2018.
@@ -17,7 +18,7 @@ class TriggerTypeConverter {
fun toTrigger(json: String): TriggerEntity {
val gson = GsonBuilder()
.registerTypeAdapter(TriggerEntity.DESERIALIZER)
- .registerTypeAdapter(TriggerEntity.KeyEntity.DESERIALIZER)
+ .registerTypeAdapter(TriggerKeyEntity.DESERIALIZER)
.registerTypeAdapter(Extra.DESERIALIZER).create()
return gson.fromJson(json)
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt
index b0e14180df..9ff2daac77 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt
@@ -8,7 +8,7 @@ import com.github.salomonbrys.kotson.byString
import com.github.salomonbrys.kotson.jsonDeserializer
import com.google.gson.annotations.SerializedName
import io.github.sds100.keymapper.data.entities.ActionEntity.Type
-import kotlinx.android.parcel.Parcelize
+import kotlinx.parcelize.Parcelize
import java.util.UUID
/**
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AssistantTriggerKeyEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AssistantTriggerKeyEntity.kt
new file mode 100644
index 0000000000..6c55705467
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AssistantTriggerKeyEntity.kt
@@ -0,0 +1,36 @@
+package io.github.sds100.keymapper.data.entities
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+import java.util.UUID
+
+@Parcelize
+data class AssistantTriggerKeyEntity(
+ /**
+ * The type of assistant that triggers this key. The voice assistant
+ * is the assistant that handles voice commands and the device assistant
+ * is the one selected in the settings as the default for reading on-screen
+ * content.
+ */
+ @SerializedName(NAME_ASSISTANT_TYPE)
+ val type: String = ASSISTANT_TYPE_ANY,
+
+ @SerializedName(NAME_CLICK_TYPE)
+ override val clickType: Int = SHORT_PRESS,
+
+ @SerializedName(NAME_UID)
+ override val uid: String = UUID.randomUUID().toString(),
+) : TriggerKeyEntity(),
+ Parcelable {
+
+ companion object {
+ // DON'T CHANGE THESE. Used for JSON serialization and parsing.
+ const val NAME_ASSISTANT_TYPE = "assistantType"
+
+ // IDS! DON'T CHANGE
+ const val ASSISTANT_TYPE_ANY = "any"
+ const val ASSISTANT_TYPE_VOICE = "voice"
+ const val ASSISTANT_TYPE_DEVICE = "device"
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/Extra.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/Extra.kt
index 8f5ea59dc8..2fd4b83b2f 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/entities/Extra.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/Extra.kt
@@ -7,7 +7,7 @@ import com.google.gson.annotations.SerializedName
import io.github.sds100.keymapper.util.Error
import io.github.sds100.keymapper.util.Result
import io.github.sds100.keymapper.util.Success
-import kotlinx.android.parcel.Parcelize
+import kotlinx.parcelize.Parcelize
/**
* Created by sds100 on 26/01/2019.
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt
new file mode 100644
index 0000000000..69d9c2ecb5
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt
@@ -0,0 +1,43 @@
+package io.github.sds100.keymapper.data.entities
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+import java.util.UUID
+
+@Parcelize
+data class KeyCodeTriggerKeyEntity(
+ @SerializedName(NAME_KEYCODE)
+ val keyCode: Int,
+
+ @SerializedName(NAME_DEVICE_ID)
+ val deviceId: String = DEVICE_ID_THIS_DEVICE,
+
+ @SerializedName(NAME_DEVICE_NAME)
+ val deviceName: String? = null,
+
+ @SerializedName(NAME_CLICK_TYPE)
+ override val clickType: Int = SHORT_PRESS,
+
+ @SerializedName(NAME_FLAGS)
+ val flags: Int = 0,
+
+ @SerializedName(NAME_UID)
+ override val uid: String = UUID.randomUUID().toString(),
+) : TriggerKeyEntity(),
+ Parcelable {
+
+ companion object {
+ // DON'T CHANGE THESE. Used for JSON serialization and parsing.
+ const val NAME_KEYCODE = "keyCode"
+ const val NAME_DEVICE_ID = "deviceId"
+ const val NAME_DEVICE_NAME = "deviceName"
+ const val NAME_FLAGS = "flags"
+
+ // IDS! DON'T CHANGE
+ const val DEVICE_ID_THIS_DEVICE = "io.github.sds100.keymapper.THIS_DEVICE"
+ const val DEVICE_ID_ANY_DEVICE = "io.github.sds100.keymapper.ANY_DEVICE"
+
+ const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt
index a8279aa957..6ab8d4ead3 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt
@@ -5,24 +5,18 @@ import androidx.annotation.IntDef
import com.github.salomonbrys.kotson.byArray
import com.github.salomonbrys.kotson.byInt
import com.github.salomonbrys.kotson.byNullableInt
-import com.github.salomonbrys.kotson.byNullableString
-import com.github.salomonbrys.kotson.byString
import com.github.salomonbrys.kotson.jsonDeserializer
import com.google.gson.annotations.SerializedName
-import kotlinx.android.parcel.Parcelize
-import java.util.UUID
+import kotlinx.parcelize.Parcelize
/**
* Created by sds100 on 16/07/2018.
*/
-/**
- * @property [keys] The key codes which will trigger the action
- */
@Parcelize
data class TriggerEntity(
@SerializedName(NAME_KEYS)
- val keys: List = listOf(),
+ val keys: List = listOf(),
@SerializedName(NAME_EXTRAS)
val extras: List = listOf(),
@@ -55,11 +49,6 @@ data class TriggerEntity(
const val DEFAULT_TRIGGER_MODE = UNDEFINED
- const val UNDETERMINED = -1
- const val SHORT_PRESS = 0
- const val LONG_PRESS = 1
- const val DOUBLE_PRESS = 2
-
const val EXTRA_SEQUENCE_TRIGGER_TIMEOUT = "extra_sequence_trigger_timeout"
const val EXTRA_LONG_PRESS_DELAY = "extra_long_press_delay"
const val EXTRA_DOUBLE_PRESS_DELAY = "extra_double_press_timeout"
@@ -67,7 +56,7 @@ data class TriggerEntity(
val DESERIALIZER = jsonDeserializer {
val triggerKeysJsonArray by it.json.byArray(NAME_KEYS)
- val keys = it.context.deserialize>(triggerKeysJsonArray)
+ val keys = it.context.deserialize>(triggerKeysJsonArray)
val extrasJsonArray by it.json.byArray(NAME_EXTRAS)
val extraList = it.context.deserialize>(extrasJsonArray) ?: listOf()
@@ -80,67 +69,6 @@ data class TriggerEntity(
}
}
- @Parcelize
- data class KeyEntity(
- @SerializedName(NAME_KEYCODE)
- val keyCode: Int,
- @SerializedName(NAME_DEVICE_ID)
- val deviceId: String = DEVICE_ID_THIS_DEVICE,
-
- @SerializedName(NAME_DEVICE_NAME)
- val deviceName: String? = null,
-
- @ClickType
- @SerializedName(NAME_CLICK_TYPE)
- val clickType: Int = SHORT_PRESS,
-
- @SerializedName(NAME_FLAGS)
- val flags: Int = 0,
-
- @SerializedName(NAME_UID)
- val uid: String = UUID.randomUUID().toString(),
- ) : Parcelable {
-
- companion object {
- // DON'T CHANGE THESE. Used for JSON serialization and parsing.
- const val NAME_KEYCODE = "keyCode"
- const val NAME_DEVICE_ID = "deviceId"
- const val NAME_DEVICE_NAME = "deviceName"
- const val NAME_CLICK_TYPE = "clickType"
- const val NAME_FLAGS = "flags"
- const val NAME_UID = "uid"
-
- // IDS! DON'T CHANGE
- const val DEVICE_ID_THIS_DEVICE = "io.github.sds100.keymapper.THIS_DEVICE"
- const val DEVICE_ID_ANY_DEVICE = "io.github.sds100.keymapper.ANY_DEVICE"
-
- const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1
-
- val DESERIALIZER = jsonDeserializer {
- val keycode by it.json.byInt(NAME_KEYCODE)
- val deviceId by it.json.byString(NAME_DEVICE_ID)
- val deviceName by it.json.byNullableString(NAME_DEVICE_NAME)
- val clickType by it.json.byInt(NAME_CLICK_TYPE)
-
- // nullable because this property was added after backup and restore was released.
- val flags by it.json.byNullableInt(NAME_FLAGS)
- val uid by it.json.byNullableString(NAME_UID)
-
- KeyEntity(
- keycode,
- deviceId,
- deviceName,
- clickType,
- flags ?: 0,
- uid ?: UUID.randomUUID().toString(),
- )
- }
- }
- }
-
@IntDef(value = [PARALLEL, SEQUENCE, UNDEFINED])
annotation class Mode
-
- @IntDef(value = [UNDETERMINED, SHORT_PRESS, LONG_PRESS, DOUBLE_PRESS])
- annotation class ClickType
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt
new file mode 100644
index 0000000000..0be4e707a2
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt
@@ -0,0 +1,72 @@
+package io.github.sds100.keymapper.data.entities
+
+import android.os.Parcelable
+import com.github.salomonbrys.kotson.byInt
+import com.github.salomonbrys.kotson.byNullableInt
+import com.github.salomonbrys.kotson.byNullableString
+import com.github.salomonbrys.kotson.byString
+import com.github.salomonbrys.kotson.jsonDeserializer
+import com.github.salomonbrys.kotson.obj
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import java.util.UUID
+
+sealed class TriggerKeyEntity : Parcelable {
+ abstract val clickType: Int
+ abstract val uid: String
+
+ companion object {
+ // DON'T CHANGE THESE. Used for JSON serialization and parsing.
+ const val NAME_CLICK_TYPE = "clickType"
+ const val NAME_UID = "uid"
+
+ // Click types
+ const val UNDETERMINED = -1
+ const val SHORT_PRESS = 0
+ const val LONG_PRESS = 1
+ const val DOUBLE_PRESS = 2
+
+ val DESERIALIZER: JsonDeserializer =
+ jsonDeserializer { (json, _, _) ->
+ // nullable because this property was added after backup and restore was released.
+ var uid: String? by json.byNullableString(key = NAME_UID)
+ uid = uid ?: UUID.randomUUID().toString()
+
+ if (json.obj.has(AssistantTriggerKeyEntity.NAME_ASSISTANT_TYPE)) {
+ return@jsonDeserializer deserializeAssistantTriggerKey(json, uid!!)
+ } else {
+ return@jsonDeserializer deserializeKeyCodeTriggerKey(json, uid!!)
+ }
+ }
+
+ private fun deserializeAssistantTriggerKey(
+ json: JsonElement,
+ uid: String,
+ ): AssistantTriggerKeyEntity {
+ val type by json.byString(AssistantTriggerKeyEntity.NAME_ASSISTANT_TYPE)
+ val clickType by json.byInt(NAME_CLICK_TYPE)
+
+ return AssistantTriggerKeyEntity(type, clickType, uid)
+ }
+
+ private fun deserializeKeyCodeTriggerKey(
+ json: JsonElement,
+ uid: String,
+ ): KeyCodeTriggerKeyEntity {
+ val keyCode by json.byInt(KeyCodeTriggerKeyEntity.NAME_KEYCODE)
+ val deviceId by json.byString(KeyCodeTriggerKeyEntity.NAME_DEVICE_ID)
+ val deviceName by json.byNullableString(KeyCodeTriggerKeyEntity.NAME_DEVICE_NAME)
+ val clickType by json.byInt(NAME_CLICK_TYPE)
+ val flags by json.byNullableInt(KeyCodeTriggerKeyEntity.NAME_FLAGS)
+
+ return KeyCodeTriggerKeyEntity(
+ keyCode,
+ deviceId,
+ deviceName,
+ clickType,
+ flags ?: 0,
+ uid,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration11To12.kt b/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration11To12.kt
index 8f0df017fa..eca08ec637 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration11To12.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration11To12.kt
@@ -98,12 +98,12 @@ object Migration11To12 {
val cursor = database.query(keyMapListQuery)
val keyMapIdColumnIndex = cursor.getColumnIndex("id")
- val keyMapTriggerColumnIndex = cursor.getColumnIndex("trigger")
+ val triggerColumnIndex = cursor.getColumnIndex("trigger")
val keyMapActionListColumnIndex = cursor.getColumnIndex("action_list")
while (cursor.moveToNext()) {
val id = cursor.getLong(keyMapIdColumnIndex)
- val triggerJson = cursor.getString(keyMapTriggerColumnIndex)
+ val triggerJson = cursor.getString(triggerColumnIndex)
val triggerJsonObject = parser.parse(triggerJson).asJsonObject
val actionListJson = cursor.getString(keyMapActionListColumnIndex)
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt b/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt
index 30e5928f55..c631cf3df7 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt
@@ -13,7 +13,7 @@ import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import io.github.sds100.keymapper.data.entities.ActionEntity
-import io.github.sds100.keymapper.data.entities.TriggerEntity
+import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity
import splitties.bitflags.hasFlag
import timber.log.Timber
@@ -107,7 +107,7 @@ object Migration1To2 {
createTriggerKey2(
it.asInt,
- TriggerEntity.KeyEntity.DEVICE_ID_ANY_DEVICE,
+ KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE,
clickType,
)
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt b/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt
index 73cca21b58..bf2378431a 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt
@@ -8,7 +8,9 @@ import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.registerTypeAdapter
import com.google.gson.Gson
import com.google.gson.GsonBuilder
+import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity
import io.github.sds100.keymapper.data.entities.TriggerEntity
+import io.github.sds100.keymapper.data.entities.TriggerKeyEntity
import splitties.bitflags.hasFlag
import splitties.bitflags.minusFlag
import splitties.bitflags.withFlag
@@ -28,7 +30,10 @@ object Migration6To7 {
.create()
query(query).apply {
- val gson = GsonBuilder().registerTypeAdapter(TriggerEntity.DESERIALIZER).create()
+ val gson = GsonBuilder()
+ .registerTypeAdapter(TriggerEntity.DESERIALIZER)
+ .registerTypeAdapter(TriggerKeyEntity.DESERIALIZER)
+ .create()
while (moveToNext()) {
val idColumnIndex = getColumnIndex("id")
@@ -38,13 +43,15 @@ object Migration6To7 {
val trigger = gson.fromJson(getString(triggerColumnIndex))
- val newTriggerKeys = trigger.keys.map {
- if (trigger.flags.hasFlag(TRIGGER_FLAG_DONT_OVERRIDE_DEFAULT_ACTION)) {
- it.copy(flags = it.flags.withFlag(TriggerEntity.KeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT))
- } else {
- it
+ val newTriggerKeys = trigger.keys
+ .mapNotNull { it as? KeyCodeTriggerKeyEntity }
+ .map { key ->
+ if (trigger.flags.hasFlag(TRIGGER_FLAG_DONT_OVERRIDE_DEFAULT_ACTION)) {
+ key.copy(flags = key.flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT))
+ } else {
+ key
+ }
}
- }
val newTriggerFlags = trigger.flags.minusFlag(
TRIGGER_FLAG_DONT_OVERRIDE_DEFAULT_ACTION,
diff --git a/app/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt b/app/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt
index 8ab8745a38..697a5c2594 100644
--- a/app/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt
@@ -1,25 +1,17 @@
package io.github.sds100.keymapper.data.repositories
import io.github.sds100.keymapper.data.db.dao.KeyMapDao
-import io.github.sds100.keymapper.data.entities.ActionEntity
-import io.github.sds100.keymapper.data.entities.Extra
import io.github.sds100.keymapper.data.entities.KeyMapEntity
-import io.github.sds100.keymapper.data.entities.TriggerEntity
import io.github.sds100.keymapper.mappings.keymaps.KeyMapRepository
-import io.github.sds100.keymapper.system.devices.DevicesAdapter
import io.github.sds100.keymapper.util.DefaultDispatcherProvider
import io.github.sds100.keymapper.util.DispatcherProvider
import io.github.sds100.keymapper.util.State
-import io.github.sds100.keymapper.util.ifIsData
import io.github.sds100.keymapper.util.splitIntoBatches
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.UUID
@@ -29,7 +21,6 @@ import java.util.UUID
*/
class RoomKeyMapRepository(
private val dao: KeyMapDao,
- private val devicesAdapter: DevicesAdapter,
private val coroutineScope: CoroutineScope,
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
) : KeyMapRepository {
@@ -40,25 +31,10 @@ class RoomKeyMapRepository(
override val keyMapList = dao.getAll()
.map { State.Data(it) }
- .map { state ->
- if (fixUnknownDeviceNamesInKeyMaps(state.data)) {
- State.Loading
- } else {
- state
- }
- }
.stateIn(coroutineScope, SharingStarted.Eagerly, State.Loading)
override val requestBackup = MutableSharedFlow>()
- init {
- keyMapList.onEach { keyMapListState ->
- keyMapListState.ifIsData {
- fixUnknownDeviceNamesInKeyMaps(it)
- }
- }.flowOn(dispatchers.default()).launchIn(coroutineScope)
- }
-
override fun insert(vararg keyMap: KeyMapEntity) {
coroutineScope.launch(dispatchers.default()) {
keyMap.splitIntoBatches(MAX_KEY_MAP_BATCH_SIZE).forEach {
@@ -128,93 +104,6 @@ class RoomKeyMapRepository(
}
}
- /**
- * See issue #612.
- * This will check if any triggers or actions have unknown device names and if the device is connected
- * then it will update the device name with the correct one.
- * This only has to check for uses of devices in Key Mapper 2.2 and older.
- *
- * @return whether any key maps were updated
- */
- private suspend fun fixUnknownDeviceNamesInKeyMaps(keyMapList: List): Boolean {
- val keyMapsToUpdate = mutableListOf()
- val connectedInputDevices =
- devicesAdapter.connectedInputDevices.first { it is State.Data } as State.Data
-
- if (connectedInputDevices.data.isEmpty()) {
- return false
- }
-
- for (keyMap in keyMapList) {
- var updateKeyMap = false
-
- val newTriggerKeys = keyMap.trigger.keys.map { triggerKey ->
- if (triggerKey.deviceId != TriggerEntity.KeyEntity.DEVICE_ID_THIS_DEVICE ||
- triggerKey.deviceId != TriggerEntity.KeyEntity.DEVICE_ID_ANY_DEVICE
- ) {
- val deviceDescriptor = triggerKey.deviceId
-
- if (triggerKey.deviceName.isNullOrBlank()) {
- val newDeviceName =
- connectedInputDevices.data.find { it.descriptor == deviceDescriptor }?.name
-
- if (newDeviceName != null) {
- updateKeyMap = true
-
- return@map triggerKey.copy(
- deviceName = newDeviceName,
- )
- }
- }
- }
-
- return@map triggerKey
- }
-
- val newActions = keyMap.actionList.map { action ->
- if (action.type == ActionEntity.Type.KEY_EVENT) {
- val deviceDescriptor =
- action.extras.find { it.id == ActionEntity.EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR }?.data
- val oldDeviceName =
- action.extras.find { it.id == ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME }?.data
-
- if (deviceDescriptor != null && oldDeviceName.isNullOrBlank()) {
- val newDeviceName =
- connectedInputDevices.data.find { it.descriptor == deviceDescriptor }?.name
-
- if (newDeviceName != null) {
- updateKeyMap = true
-
- val newExtras = action.extras.toMutableList().apply {
- removeAll { it.id == ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME }
- add(Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME, newDeviceName))
- }
-
- return@map action.copy(extras = newExtras)
- }
- }
- }
-
- return@map action
- }
-
- if (updateKeyMap) {
- val newKeyMap = keyMap.copy(
- trigger = keyMap.trigger.copy(keys = newTriggerKeys),
- actionList = newActions,
- )
-
- keyMapsToUpdate.add(newKeyMap)
- }
- }
-
- if (keyMapsToUpdate.isNotEmpty()) {
- dao.update(*keyMapsToUpdate.toTypedArray())
- }
-
- return keyMapsToUpdate.isNotEmpty()
- }
-
private fun requestBackup() {
coroutineScope.launch {
val keyMapList = keyMapList.first { it is State.Data } as State.Data
diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeFragment.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeFragment.kt
index ea54c30689..ec1f4ba102 100644
--- a/app/src/main/java/io/github/sds100/keymapper/home/HomeFragment.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeFragment.kt
@@ -16,14 +16,11 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.widget.ViewPager2
-import androidx.work.OneTimeWorkRequestBuilder
-import androidx.work.WorkManager
import com.google.android.material.bottomappbar.BottomAppBar.FAB_ALIGNMENT_MODE_CENTER
import com.google.android.material.bottomappbar.BottomAppBar.FAB_ALIGNMENT_MODE_END
import com.google.android.material.tabs.TabLayoutMediator
import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.backup.BackupUtils
-import io.github.sds100.keymapper.data.db.SeedDatabaseWorker
import io.github.sds100.keymapper.databinding.FragmentHomeBinding
import io.github.sds100.keymapper.fixError
import io.github.sds100.keymapper.success
@@ -33,7 +30,6 @@ import io.github.sds100.keymapper.util.Inject
import io.github.sds100.keymapper.util.QuickStartGuideTapTarget
import io.github.sds100.keymapper.util.launchRepeatOnLifecycle
import io.github.sds100.keymapper.util.str
-import io.github.sds100.keymapper.util.strArray
import io.github.sds100.keymapper.util.ui.TextListItem
import io.github.sds100.keymapper.util.ui.setupNavigation
import io.github.sds100.keymapper.util.ui.showPopups
@@ -130,7 +126,8 @@ class HomeFragment : Fragment() {
binding.viewPager.adapter = pagerAdapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
- tab.text = strArray(R.array.home_tab_titles)[position]
+ val tabId = homeViewModel.tabsState.value.tabs[position]
+ tab.text = str(HomePagerAdapter.TAB_NAMES[tabId]!!)
}.apply {
attach()
}
@@ -147,12 +144,6 @@ class HomeFragment : Fragment() {
true
}
- R.id.action_seed_database -> {
- val request = OneTimeWorkRequestBuilder().build()
- WorkManager.getInstance(requireContext()).enqueue(request)
- true
- }
-
R.id.action_select_all -> {
homeViewModel.onSelectAllClick()
true
diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomePagerAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomePagerAdapter.kt
index 75c15ed552..22b272df85 100644
--- a/app/src/main/java/io/github/sds100/keymapper/home/HomePagerAdapter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/home/HomePagerAdapter.kt
@@ -2,6 +2,7 @@ package io.github.sds100.keymapper.home
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
+import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintMapListFragment
import io.github.sds100.keymapper.mappings.keymaps.KeyMapListFragment
@@ -13,7 +14,14 @@ class HomePagerAdapter(
fragment: Fragment,
) : FragmentStateAdapter(fragment) {
- private var tabs: Set = emptySet()
+ companion object {
+ val TAB_NAMES: Map = mapOf(
+ HomeTab.KEY_EVENTS to R.string.tab_keyevents,
+ HomeTab.FINGERPRINT_MAPS to R.string.tab_fingerprint,
+ )
+ }
+
+ private var tabs: List = emptyList()
private val tabFragmentsCreators: List<() -> Fragment>
get() = tabs.map { tab ->
when (tab) {
@@ -39,7 +47,7 @@ class HomePagerAdapter(
override fun createFragment(position: Int) = tabFragmentsCreators[position].invoke()
- fun invalidateFragments(tabs: Set) {
+ fun invalidateFragments(tabs: List) {
if (this.tabs == tabs) return
this.tabs = tabs
diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt
index a04c79afd9..b3f6f3d6c1 100644
--- a/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt
@@ -144,7 +144,7 @@ class HomeViewModel(
if (showFingerprintMaps) {
yield(HomeTab.FINGERPRINT_MAPS)
}
- }.toSet()
+ }.toList()
val showTabs = when {
tabs.size == 1 -> false
@@ -165,7 +165,7 @@ class HomeViewModel(
HomeTabsState(
enableViewPagerSwiping = false,
showTabs = false,
- emptySet(),
+ emptyList(),
),
)
@@ -541,7 +541,7 @@ enum class HomeAppBarState {
data class HomeTabsState(
val enableViewPagerSwiping: Boolean = true,
val showTabs: Boolean = false,
- val tabs: Set,
+ val tabs: List,
)
data class HomeErrorListState(
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/ConfigMappingFragment.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/ConfigMappingFragment.kt
index 45c4e660f5..bc909bba47 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/ConfigMappingFragment.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/ConfigMappingFragment.kt
@@ -78,6 +78,10 @@ abstract class ConfigMappingFragment : Fragment() {
fragmentInfoList.map { it.first.toLong() to it.second.instantiate },
)
+ // Don't show the swipe animations for reaching the end of the pager
+ // if there is only one page.
+ binding.viewPager.isUserInputEnabled = fragmentInfoList.size > 1
+
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
val tabTitleRes = fragmentInfoList[position].second.header
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt
index 7753ee2a19..212149d0f3 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt
@@ -4,6 +4,7 @@ import io.github.sds100.keymapper.actions.Action
import io.github.sds100.keymapper.actions.PerformActionsUseCase
import io.github.sds100.keymapper.actions.RepeatMode
import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase
+import io.github.sds100.keymapper.constraints.isSatisfied
import io.github.sds100.keymapper.data.PreferenceDefaults
import io.github.sds100.keymapper.util.InputEventType
import kotlinx.coroutines.CoroutineScope
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapFragment.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapFragment.kt
index af4caac18c..94d7f0f4ab 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapFragment.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapFragment.kt
@@ -50,6 +50,10 @@ class ConfigKeyMapFragment : ConfigMappingFragment() {
viewModel.loadKeymap(it)
}
}
+
+ if (args.showAdvancedTriggers) {
+ viewModel.configTriggerViewModel.showAdvancedTriggersBottomSheet = true
+ }
}
viewModel.configTriggerViewModel.setupNavigation(this)
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt
index 8746b8556a..83ee68fe9f 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt
@@ -9,7 +9,10 @@ import io.github.sds100.keymapper.data.repositories.PreferenceRepository
import io.github.sds100.keymapper.mappings.BaseConfigMappingUseCase
import io.github.sds100.keymapper.mappings.ClickType
import io.github.sds100.keymapper.mappings.ConfigMappingUseCase
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTrigger
+import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerType
+import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode
@@ -38,36 +41,54 @@ class ConfigKeyMapUseCaseImpl(
private val showDeviceDescriptors: Flow =
preferenceRepository.get(Keys.showDeviceDescriptors).map { it ?: false }
- override fun addTriggerKey(
- keyCode: Int,
- device: TriggerKeyDevice,
- ) = editTrigger { trigger ->
+ override fun addAssistantTriggerKey(type: AssistantTriggerType) = editTrigger { trigger ->
val clickType = when (trigger.mode) {
is TriggerMode.Parallel -> trigger.mode.clickType
TriggerMode.Sequence -> ClickType.SHORT_PRESS
TriggerMode.Undefined -> ClickType.SHORT_PRESS
}
- val containsKey = trigger.keys.any { keyToCompare ->
- if (trigger.mode != TriggerMode.Sequence) {
- val sameKeyCode = keyCode == keyToCompare.keyCode
+ // Check whether the trigger already contains the key because if so
+ // then it must be converted to a sequence trigger.
+ val containsKey = trigger.keys.any { it is AssistantTriggerKey }
- // if the new key is not external, check whether a trigger key already exists for this device
- val sameDevice = when {
- keyToCompare.device is TriggerKeyDevice.External &&
- device is TriggerKeyDevice.External ->
- keyToCompare.device.descriptor == device.descriptor
+ val triggerKey = AssistantTriggerKey(type = type, clickType = clickType)
- else -> true
- }
+ val newKeys = trigger.keys.plus(triggerKey)
- sameKeyCode && sameDevice
- } else {
- false
- }
+ val newMode = when {
+ trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence
+ newKeys.size <= 1 -> TriggerMode.Undefined
+
+ /* Automatically make it a parallel trigger when the user makes a trigger with more than one key
+ because this is what most users are expecting when they make a trigger with multiple keys.
+
+ It must be a short press because long pressing the assistant key isn't supported.
+ */
+ !containsKey -> TriggerMode.Parallel(ClickType.SHORT_PRESS)
+ else -> trigger.mode
+ }
+
+ trigger.copy(keys = newKeys, mode = newMode)
+ }
+
+ override fun addKeyCodeTriggerKey(
+ keyCode: Int,
+ device: TriggerKeyDevice,
+ ) = editTrigger { trigger ->
+ val clickType = when (trigger.mode) {
+ is TriggerMode.Parallel -> trigger.mode.clickType
+ TriggerMode.Sequence -> ClickType.SHORT_PRESS
+ TriggerMode.Undefined -> ClickType.SHORT_PRESS
}
- val newKeys = trigger.keys.toMutableList()
+ // Check whether the trigger already contains the key because if so
+ // then it must be converted to a sequence trigger.
+ val containsKey = trigger.keys
+ .mapNotNull { it as? KeyCodeTriggerKey }
+ .any { keyToCompare ->
+ keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device)
+ }
var consumeKeyEvent = true
@@ -76,22 +97,22 @@ class ConfigKeyMapUseCaseImpl(
consumeKeyEvent = false
}
- val triggerKey = TriggerKey(
+ val triggerKey = KeyCodeTriggerKey(
keyCode = keyCode,
device = device,
clickType = clickType,
- consumeKeyEvent = consumeKeyEvent,
+ consumeEvent = consumeKeyEvent,
)
- newKeys.add(triggerKey)
+ val newKeys = trigger.keys.plus(triggerKey)
val newMode = when {
- containsKey -> TriggerMode.Sequence
+ trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence
newKeys.size <= 1 -> TriggerMode.Undefined
/* Automatically make it a parallel trigger when the user makes a trigger with more than one key
because this is what most users are expecting when they make a trigger with multiple keys */
- newKeys.size == 2 && !containsKey -> TriggerMode.Parallel(clickType)
+ newKeys.size == 2 && !containsKey -> TriggerMode.Parallel(triggerKey.clickType)
else -> trigger.mode
}
@@ -119,8 +140,14 @@ class ConfigKeyMapUseCaseImpl(
)
}
+ override fun getTriggerKey(uid: String): TriggerKey? {
+ return mapping.value.dataOrNull()?.trigger?.keys?.find { it.uid == uid }
+ }
+
override fun setParallelTriggerMode() = editTrigger { trigger ->
- if (trigger.mode is TriggerMode.Parallel) return@editTrigger trigger
+ if (trigger.mode is TriggerMode.Parallel) {
+ return@editTrigger trigger
+ }
// undefined mode only allowed if one or no keys
if (trigger.keys.size <= 1) {
@@ -128,19 +155,20 @@ class ConfigKeyMapUseCaseImpl(
}
val oldKeys = trigger.keys
- var newKeys = oldKeys.toMutableList()
-
- if (trigger.mode !is TriggerMode.Parallel) {
- // set all the keys to a short press if coming from a non-parallel trigger
- // because they must all be the same click type and can't all be double pressed
- newKeys = newKeys.map { key ->
- key.copy(clickType = ClickType.SHORT_PRESS)
- }.toMutableList()
+ var newKeys = oldKeys
+ // set all the keys to a short press if coming from a non-parallel trigger
+ // because they must all be the same click type and can't all be double pressed
+ newKeys = newKeys
+ .map { key -> key.setClickType(clickType = ClickType.SHORT_PRESS) }
// remove duplicates of keys that have the same keycode and device id
- newKeys =
- newKeys.distinctBy { Pair(it.keyCode, it.device) }.toMutableList()
- }
+ .distinctBy { key ->
+ when (key) {
+ // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time
+ is AssistantTriggerKey -> 0
+ is KeyCodeTriggerKey -> Pair(key.keyCode, key.device)
+ }
+ }
val newMode = if (newKeys.size <= 1) {
TriggerMode.Undefined
@@ -178,7 +206,7 @@ class ConfigKeyMapUseCaseImpl(
return@editTrigger oldTrigger
}
- val newKeys = oldTrigger.keys.map { it.copy(clickType = ClickType.SHORT_PRESS) }
+ val newKeys = oldTrigger.keys.map { it.setClickType(clickType = ClickType.SHORT_PRESS) }
val newMode = if (newKeys.size <= 1) {
TriggerMode.Undefined
} else {
@@ -189,50 +217,75 @@ class ConfigKeyMapUseCaseImpl(
}
override fun setTriggerLongPress() {
- editTrigger { oldTrigger ->
- if (oldTrigger.mode == TriggerMode.Sequence) {
- return@editTrigger oldTrigger
+ editTrigger { trigger ->
+ if (trigger.mode == TriggerMode.Sequence) {
+ return@editTrigger trigger
}
- val newKeys = oldTrigger.keys.map { it.copy(clickType = ClickType.LONG_PRESS) }
+ // You can't set the trigger to a long press if it contains a key
+ // that isn't detected with key codes. This is because there aren't
+ // separate key events for the up and down press that can be timed.
+ if (trigger.keys.any { it !is KeyCodeTriggerKey }) {
+ return@editTrigger trigger
+ }
+
+ val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.LONG_PRESS) }
val newMode = if (newKeys.size <= 1) {
TriggerMode.Undefined
} else {
TriggerMode.Parallel(ClickType.LONG_PRESS)
}
- oldTrigger.copy(keys = newKeys, mode = newMode)
+ trigger.copy(keys = newKeys, mode = newMode)
}
}
override fun setTriggerDoublePress() {
- editTrigger { oldTrigger ->
- if (oldTrigger.mode != TriggerMode.Undefined) {
- return@editTrigger oldTrigger
+ editTrigger { trigger ->
+ if (trigger.mode != TriggerMode.Undefined) {
+ return@editTrigger trigger
}
- val newKeys = oldTrigger.keys.map { it.copy(clickType = ClickType.DOUBLE_PRESS) }
+ val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) }
val newMode = TriggerMode.Undefined
- oldTrigger.copy(keys = newKeys, mode = newMode)
+ trigger.copy(keys = newKeys, mode = newMode)
}
}
override fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) {
- editTriggerKey(keyUid) {
- it.copy(clickType = clickType)
+ editTriggerKey(keyUid) { key ->
+ key.setClickType(clickType = clickType)
}
}
override fun setTriggerKeyDevice(keyUid: String, device: TriggerKeyDevice) {
- editTriggerKey(keyUid) {
- it.copy(device = device)
+ editTriggerKey(keyUid) { key ->
+ if (key is KeyCodeTriggerKey) {
+ key.copy(device = device)
+ } else {
+ key
+ }
}
}
override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) {
- editTriggerKey(keyUid) {
- it.copy(consumeKeyEvent = consumeKeyEvent)
+ editTriggerKey(keyUid) { key ->
+ if (key is KeyCodeTriggerKey) {
+ key.copy(consumeEvent = consumeKeyEvent)
+ } else {
+ key
+ }
+ }
+ }
+
+ override fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) {
+ editTriggerKey(keyUid) { key ->
+ if (key is AssistantTriggerKey) {
+ key.copy(type = type)
+ } else {
+ key
+ }
}
}
@@ -435,7 +488,7 @@ class ConfigKeyMapUseCaseImpl(
}
}
- private fun editTrigger(block: (trigger: KeyMapTrigger) -> KeyMapTrigger) {
+ private fun editTrigger(block: (trigger: Trigger) -> Trigger) {
editKeyMap { keyMap ->
val newTrigger = block(keyMap.trigger)
@@ -464,8 +517,10 @@ class ConfigKeyMapUseCaseImpl(
interface ConfigKeyMapUseCase : ConfigMappingUseCase {
// trigger
- fun addTriggerKey(keyCode: Int, device: TriggerKeyDevice)
+ fun addKeyCodeTriggerKey(keyCode: Int, device: TriggerKeyDevice)
+ fun addAssistantTriggerKey(type: AssistantTriggerType)
fun removeTriggerKey(uid: String)
+ fun getTriggerKey(uid: String): TriggerKey?
fun moveTriggerKey(fromIndex: Int, toIndex: Int)
fun restoreState(keyMap: KeyMap)
@@ -483,6 +538,7 @@ interface ConfigKeyMapUseCase : ConfigMappingUseCase {
fun setTriggerKeyClickType(keyUid: String, clickType: ClickType)
fun setTriggerKeyDevice(keyUid: String, device: TriggerKeyDevice)
fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean)
+ fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType)
fun setVibrateEnabled(enabled: Boolean)
fun setVibrationDuration(duration: Defaultable)
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt
index 9b4366b199..8575a29006 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt
@@ -12,8 +12,10 @@ import io.github.sds100.keymapper.constraints.ConstraintUtils
import io.github.sds100.keymapper.mappings.ConfigMappingUiState
import io.github.sds100.keymapper.mappings.ConfigMappingViewModel
import io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerKeyViewModel
+import io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel
import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerUseCase
import io.github.sds100.keymapper.onboarding.OnboardingUseCase
+import io.github.sds100.keymapper.purchasing.PurchasingManager
import io.github.sds100.keymapper.ui.utils.getJsonSerializable
import io.github.sds100.keymapper.ui.utils.putJsonSerializable
import io.github.sds100.keymapper.util.State
@@ -37,6 +39,7 @@ class ConfigKeyMapViewModel(
private val displayMapping: DisplayKeyMapUseCase,
createActionUseCase: CreateActionUseCase,
resourceProvider: ResourceProvider,
+ purchasingManager: PurchasingManager,
) : ViewModel(),
ConfigMappingViewModel,
ResourceProvider by resourceProvider {
@@ -61,7 +64,7 @@ class ConfigKeyMapViewModel(
resourceProvider,
)
- val configTriggerViewModel = ConfigKeyMapTriggerViewModel(
+ val configTriggerViewModel = ConfigTriggerViewModel(
viewModelScope,
onboarding,
config,
@@ -69,6 +72,7 @@ class ConfigKeyMapViewModel(
createKeyMapShortcut,
displayMapping,
resourceProvider,
+ purchasingManager,
)
override val configConstraintsViewModel = ConfigConstraintsViewModel(
@@ -129,6 +133,7 @@ class ConfigKeyMapViewModel(
private val displayMapping: DisplayKeyMapUseCase,
private val createActionUseCase: CreateActionUseCase,
private val resourceProvider: ResourceProvider,
+ private val purchasingManager: PurchasingManager,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@@ -142,6 +147,7 @@ class ConfigKeyMapViewModel(
displayMapping,
createActionUseCase,
resourceProvider,
+ purchasingManager,
) as T
}
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt
index cb9eac6c7a..9b95283341 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt
@@ -5,11 +5,16 @@ import android.view.KeyEvent
import io.github.sds100.keymapper.data.Keys
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
import io.github.sds100.keymapper.mappings.DisplaySimpleMappingUseCase
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTriggerError
+import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError
+import io.github.sds100.keymapper.purchasing.ProductId
+import io.github.sds100.keymapper.purchasing.PurchasingManager
import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter
import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeHelper
import io.github.sds100.keymapper.system.permissions.Permission
import io.github.sds100.keymapper.system.permissions.PermissionAdapter
+import io.github.sds100.keymapper.util.valueIfFailure
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
@@ -24,6 +29,7 @@ class DisplayKeyMapUseCaseImpl(
private val inputMethodAdapter: InputMethodAdapter,
displaySimpleMappingUseCase: DisplaySimpleMappingUseCase,
private val preferenceRepository: PreferenceRepository,
+ private val purchasingManager: PurchasingManager,
) : DisplayKeyMapUseCase,
DisplaySimpleMappingUseCase by displaySimpleMappingUseCase {
private companion object {
@@ -35,25 +41,29 @@ class DisplayKeyMapUseCaseImpl(
private val keyMapperImeHelper: KeyMapperImeHelper = KeyMapperImeHelper(inputMethodAdapter)
- override val invalidateTriggerErrors = merge(
+ override val invalidateTriggerErrors: Flow = merge(
permissionAdapter.onPermissionsUpdate,
preferenceRepository.get(Keys.neverShowDndError).map { }.drop(1),
+ purchasingManager.onCompleteProductPurchase.map { },
)
- override suspend fun getTriggerErrors(keyMap: KeyMap): List {
+ override suspend fun getTriggerErrors(keyMap: KeyMap): List {
val trigger = keyMap.trigger
- val errors = mutableListOf()
-
+ val errors = mutableListOf()
// can only detect volume button presses during a phone call with an input method service
if (!keyMapperImeHelper.isCompatibleImeChosen() && keyMap.requiresImeKeyEventForwarding()) {
- errors.add(KeyMapTriggerError.CANT_DETECT_IN_PHONE_CALL)
+ errors.add(TriggerError.CANT_DETECT_IN_PHONE_CALL)
}
- if (trigger.keys.any { it.keyCode in keysThatRequireDndAccess }) {
+ val requiresDndAccess = trigger.keys
+ .mapNotNull { it as? KeyCodeTriggerKey }
+ .any { it.keyCode in keysThatRequireDndAccess }
+
+ if (requiresDndAccess) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
!permissionAdapter.isGranted(Permission.ACCESS_NOTIFICATION_POLICY)
) {
- errors.add(KeyMapTriggerError.DND_ACCESS_DENIED)
+ errors.add(TriggerError.DND_ACCESS_DENIED)
}
}
@@ -61,7 +71,27 @@ class DisplayKeyMapUseCaseImpl(
!permissionAdapter.isGranted(Permission.ROOT) &&
trigger.isDetectingWhenScreenOffAllowed()
) {
- errors.add(KeyMapTriggerError.SCREEN_OFF_ROOT_DENIED)
+ errors.add(TriggerError.SCREEN_OFF_ROOT_DENIED)
+ }
+
+ val containsAssistantTrigger = keyMap.trigger.keys.any { it is AssistantTriggerKey }
+ val containsDeviceAssistantTrigger =
+ keyMap.trigger.keys.any { it is AssistantTriggerKey && it.requiresDeviceAssistant() }
+
+ val isAssistantTriggerPurchased =
+ purchasingManager.isPurchased(ProductId.ASSISTANT_TRIGGER).valueIfFailure { false }
+
+ if (containsAssistantTrigger && !isAssistantTriggerPurchased) {
+ errors.add(TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED)
+ }
+
+ val isKeyMapperDeviceAssistant = permissionAdapter.isGranted(Permission.DEVICE_ASSISTANT)
+
+ // Show an error if Key Mapper isn't selected as the device assistant
+ // and an assistant trigger is used. The error shouldn't be shown
+ // if the assistant trigger feature is not purchased.
+ if (containsDeviceAssistantTrigger && isAssistantTriggerPurchased && !isKeyMapperDeviceAssistant) {
+ errors.add(TriggerError.ASSISTANT_NOT_SELECTED)
}
return errors
@@ -70,5 +100,5 @@ class DisplayKeyMapUseCaseImpl(
interface DisplayKeyMapUseCase : DisplaySimpleMappingUseCase {
val invalidateTriggerErrors: Flow
- suspend fun getTriggerErrors(keyMap: KeyMap): List
+ suspend fun getTriggerErrors(keyMap: KeyMap): List
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMap.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMap.kt
index 48099c3dad..edf866e9d6 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMap.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMap.kt
@@ -9,8 +9,9 @@ import io.github.sds100.keymapper.constraints.ConstraintState
import io.github.sds100.keymapper.data.entities.KeyMapEntity
import io.github.sds100.keymapper.mappings.Mapping
import io.github.sds100.keymapper.mappings.keymaps.detection.KeyMapController
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTrigger
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeymapTriggerEntityMapper
+import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger
+import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerEntityMapper
import kotlinx.serialization.Serializable
import java.util.UUID
@@ -22,7 +23,7 @@ import java.util.UUID
data class KeyMap(
val dbId: Long? = null,
val uid: String = UUID.randomUUID().toString(),
- val trigger: KeyMapTrigger = KeyMapTrigger(),
+ val trigger: Trigger = Trigger(),
override val actionList: List = emptyList(),
override val constraintState: ConstraintState = ConstraintState(),
override val isEnabled: Boolean = true,
@@ -62,13 +63,25 @@ data class KeyMap(
}
/**
- * @return whether this key map requires an input method to send the key events
- * because otherwise it won't be detected.
+ * Whether this key map requires an input method to detect the key events.
+ * If the key map needs to answer or end a call then it must use an input method to detect
+ * the key events because volume key events are not sent to accessibility services when a call
+ * is incoming.
*/
-fun KeyMap.requiresImeKeyEventForwarding(): Boolean =
- trigger.keys.any { it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || it.keyCode == KeyEvent.KEYCODE_VOLUME_UP } &&
+fun KeyMap.requiresImeKeyEventForwarding(): Boolean {
+ val hasPhoneCallAction =
actionList.any { it.data is ActionData.AnswerCall || it.data is ActionData.EndCall }
+ val hasVolumeKeys = trigger.keys
+ .mapNotNull { it as? KeyCodeTriggerKey }
+ .any {
+ it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
+ it.keyCode == KeyEvent.KEYCODE_VOLUME_UP
+ }
+
+ return hasVolumeKeys && hasPhoneCallAction
+}
+
object KeyMapEntityMapper {
fun fromEntity(entity: KeyMapEntity): KeyMap {
val actionList = entity.actionList.mapNotNull { KeymapActionEntityMapper.fromEntity(it) }
@@ -81,7 +94,7 @@ object KeyMapEntityMapper {
return KeyMap(
dbId = entity.id,
uid = entity.uid,
- trigger = KeymapTriggerEntityMapper.fromEntity(entity.trigger),
+ trigger = TriggerEntityMapper.fromEntity(entity.trigger),
actionList = actionList,
constraintState = ConstraintState(constraintList, constraintMode),
isEnabled = entity.isEnabled,
@@ -93,7 +106,7 @@ object KeyMapEntityMapper {
return KeyMapEntity(
id = dbId,
- trigger = KeymapTriggerEntityMapper.toEntity(keyMap.trigger),
+ trigger = TriggerEntityMapper.toEntity(keyMap.trigger),
actionList = actionEntityList,
constraintList = keyMap.constraintState.constraints.map {
ConstraintEntityMapper.toEntity(
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt
index 38b3502647..931e880d87 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt
@@ -3,10 +3,14 @@ package io.github.sds100.keymapper.mappings.keymaps
import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.mappings.BaseMappingListItemCreator
import io.github.sds100.keymapper.mappings.ClickType
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTrigger
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTriggerError
+import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerType
+import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger
+import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode
+import io.github.sds100.keymapper.purchasing.ProductId
import io.github.sds100.keymapper.system.devices.InputDeviceUtils
import io.github.sds100.keymapper.system.keyevents.KeyEventUtils
import io.github.sds100.keymapper.system.permissions.Permission
@@ -25,13 +29,17 @@ class KeyMapListItemCreator(
KeyMapActionUiHelper(displayMapping, resourceProvider),
resourceProvider,
) {
+ private val midDot by lazy { getString(R.string.middot) }
+ private val longPressString by lazy { getString(R.string.clicktype_long_press) }
+ private val doublePressString by lazy { getString(R.string.clicktype_double_press) }
+ private val anyAssistantString by lazy { getString(R.string.assistant_any_trigger_name) }
+ private val voiceAssistantString by lazy { getString(R.string.assistant_voice_trigger_name) }
+ private val deviceAssistantString by lazy { getString(R.string.assistant_device_trigger_name) }
suspend fun create(
keyMap: KeyMap,
showDeviceDescriptors: Boolean,
): KeyMapListItem.KeyMapUiState {
- val midDot = getString(R.string.middot)
-
val triggerDescription = buildString {
val separator = when (keyMap.trigger.mode) {
is TriggerMode.Parallel -> getString(R.string.plus)
@@ -39,46 +47,15 @@ class KeyMapListItemCreator(
is TriggerMode.Undefined -> null
}
- val longPressString = getString(R.string.clicktype_long_press)
- val doublePressString = getString(R.string.clicktype_double_press)
-
keyMap.trigger.keys.forEachIndexed { index, key ->
if (index > 0) {
append(" $separator ")
}
- when (key.clickType) {
- ClickType.LONG_PRESS -> append(longPressString).append(" ")
- ClickType.DOUBLE_PRESS -> append(doublePressString).append(" ")
- else -> Unit
- }
-
- append(KeyEventUtils.keyCodeToString(key.keyCode))
-
- val deviceName = when (key.device) {
- is TriggerKeyDevice.Internal -> getString(R.string.this_device)
- is TriggerKeyDevice.Any -> getString(R.string.any_device)
- is TriggerKeyDevice.External -> {
- if (showDeviceDescriptors) {
- InputDeviceUtils.appendDeviceDescriptorToName(
- key.device.descriptor,
- key.device.name,
- )
- } else {
- key.device.name
- }
- }
- }
-
- append(" (")
-
- append(deviceName)
-
- if (!key.consumeKeyEvent) {
- append(" $midDot ${getString(R.string.flag_dont_override_default_action)}")
+ when (key) {
+ is AssistantTriggerKey -> appendAssistantTriggerKeyName(key)
+ is KeyCodeTriggerKey -> appendKeyCodeTriggerKeyName(key, showDeviceDescriptors)
}
-
- append(")")
}
}
@@ -109,28 +86,7 @@ class KeyMapListItemCreator(
val triggerErrors = displayMapping.getTriggerErrors(keyMap)
- val triggerErrorChips = triggerErrors.map {
- when (it) {
- KeyMapTriggerError.DND_ACCESS_DENIED ->
- ChipUi.Error(
- id = KeyMapTriggerError.DND_ACCESS_DENIED.toString(),
- text = getString(R.string.trigger_error_dnd_access_denied_short),
- error = Error.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY),
- )
-
- KeyMapTriggerError.SCREEN_OFF_ROOT_DENIED -> ChipUi.Error(
- id = KeyMapTriggerError.SCREEN_OFF_ROOT_DENIED.toString(),
- text = getString(R.string.trigger_error_screen_off_root_permission_denied_short),
- error = Error.PermissionDenied(Permission.ROOT),
- )
-
- KeyMapTriggerError.CANT_DETECT_IN_PHONE_CALL -> ChipUi.Error(
- id = KeyMapTriggerError.SCREEN_OFF_ROOT_DENIED.toString(),
- text = getString(R.string.trigger_error_cant_detect_in_phone_call),
- error = Error.CantDetectKeyEventsInPhoneCall,
- )
- }
- }
+ val triggerErrorChips = triggerErrors.map(this::getTriggerChipError)
return KeyMapListItem.KeyMapUiState(
uid = keyMap.uid,
@@ -143,7 +99,92 @@ class KeyMapListItemCreator(
)
}
- private fun getTriggerOptionLabels(trigger: KeyMapTrigger): List {
+ private fun getTriggerChipError(error: TriggerError): ChipUi.Error =
+ when (error) {
+ TriggerError.DND_ACCESS_DENIED ->
+ ChipUi.Error(
+ id = TriggerError.DND_ACCESS_DENIED.toString(),
+ text = getString(R.string.trigger_error_dnd_access_denied_short),
+ error = Error.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY),
+ )
+
+ TriggerError.SCREEN_OFF_ROOT_DENIED -> ChipUi.Error(
+ id = TriggerError.SCREEN_OFF_ROOT_DENIED.toString(),
+ text = getString(R.string.trigger_error_screen_off_root_permission_denied_short),
+ error = Error.PermissionDenied(Permission.ROOT),
+ )
+
+ TriggerError.CANT_DETECT_IN_PHONE_CALL -> ChipUi.Error(
+ id = TriggerError.SCREEN_OFF_ROOT_DENIED.toString(),
+ text = getString(R.string.trigger_error_cant_detect_in_phone_call),
+ error = Error.CantDetectKeyEventsInPhoneCall,
+ )
+
+ TriggerError.ASSISTANT_NOT_SELECTED -> ChipUi.Error(
+ id = error.toString(),
+ text = getString(R.string.trigger_error_assistant_activity_not_chosen),
+ error = Error.PermissionDenied(Permission.DEVICE_ASSISTANT),
+ )
+
+ TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> ChipUi.Error(
+ id = error.toString(),
+ text = getString(R.string.trigger_error_assistant_not_purchased),
+ error = Error.ProductNotPurchased(ProductId.ASSISTANT_TRIGGER),
+ )
+ }
+
+ private fun StringBuilder.appendKeyCodeTriggerKeyName(
+ key: KeyCodeTriggerKey,
+ showDeviceDescriptors: Boolean,
+ ) {
+ when (key.clickType) {
+ ClickType.LONG_PRESS -> append(longPressString).append(" ")
+ ClickType.DOUBLE_PRESS -> append(doublePressString).append(" ")
+ else -> Unit
+ }
+
+ append(KeyEventUtils.keyCodeToString(key.keyCode))
+
+ val deviceName = when (key.device) {
+ is TriggerKeyDevice.Internal -> getString(R.string.this_device)
+ is TriggerKeyDevice.Any -> getString(R.string.any_device)
+ is TriggerKeyDevice.External -> {
+ if (showDeviceDescriptors) {
+ InputDeviceUtils.appendDeviceDescriptorToName(
+ key.device.descriptor,
+ key.device.name,
+ )
+ } else {
+ key.device.name
+ }
+ }
+ }
+
+ append(" (")
+
+ append(deviceName)
+
+ if (!key.consumeEvent) {
+ append(" $midDot ${getString(R.string.flag_dont_override_default_action)}")
+ }
+
+ append(")")
+ }
+
+ private fun StringBuilder.appendAssistantTriggerKeyName(key: AssistantTriggerKey) {
+ when (key.clickType) {
+ ClickType.DOUBLE_PRESS -> append(doublePressString).append(" ")
+ else -> Unit
+ }
+
+ when (key.type) {
+ AssistantTriggerType.ANY -> append(anyAssistantString)
+ AssistantTriggerType.VOICE -> append(voiceAssistantString)
+ AssistantTriggerType.DEVICE -> append(deviceAssistantString)
+ }
+ }
+
+ private fun getTriggerOptionLabels(trigger: Trigger): List {
val labels = mutableListOf()
if (trigger.isVibrateAllowed() && trigger.vibrate) {
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsController.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt
similarity index 91%
rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsController.kt
rename to app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt
index 8863c23a11..a60d8b04cb 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsController.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt
@@ -1,9 +1,9 @@
-package io.github.sds100.keymapper.mappings.keymaps
+package io.github.sds100.keymapper.mappings.keymaps.detection
import io.github.sds100.keymapper.actions.PerformActionsUseCase
import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase
import io.github.sds100.keymapper.mappings.SimpleMappingController
-import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase
+import io.github.sds100.keymapper.mappings.keymaps.KeyMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt
new file mode 100644
index 0000000000..fad2475b4c
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt
@@ -0,0 +1,76 @@
+package io.github.sds100.keymapper.mappings.keymaps.trigger
+
+import io.github.sds100.keymapper.data.entities.AssistantTriggerKeyEntity
+import io.github.sds100.keymapper.data.entities.TriggerKeyEntity
+import io.github.sds100.keymapper.mappings.ClickType
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.util.UUID
+
+@Serializable
+data class AssistantTriggerKey(
+ override val uid: String = UUID.randomUUID().toString(),
+
+ // A custom JSON name is required because this conflicts with the built-in "type" property.
+ @SerialName("assistantType")
+ val type: AssistantTriggerType,
+
+ override val clickType: ClickType,
+) : TriggerKey() {
+
+ // This is always true for an assistant key event because Key Mapper can't forward the
+ // assistant event to another app (or can it??).
+ override val consumeEvent: Boolean = true
+
+ /**
+ * Whether this assistant trigger requires the device assistant activity to be set.
+ */
+ fun requiresDeviceAssistant(): Boolean {
+ return type == AssistantTriggerType.DEVICE || type == AssistantTriggerType.ANY
+ }
+
+ companion object {
+ fun fromEntity(
+ entity: AssistantTriggerKeyEntity,
+ ): TriggerKey {
+ val type: AssistantTriggerType = when (entity.type) {
+ AssistantTriggerKeyEntity.ASSISTANT_TYPE_VOICE -> AssistantTriggerType.VOICE
+ AssistantTriggerKeyEntity.ASSISTANT_TYPE_DEVICE -> AssistantTriggerType.DEVICE
+ else -> AssistantTriggerType.ANY
+ }
+
+ val clickType: ClickType = when (entity.clickType) {
+ TriggerKeyEntity.SHORT_PRESS -> ClickType.SHORT_PRESS
+ TriggerKeyEntity.LONG_PRESS -> ClickType.LONG_PRESS
+ TriggerKeyEntity.DOUBLE_PRESS -> ClickType.DOUBLE_PRESS
+ else -> ClickType.SHORT_PRESS
+ }
+
+ return AssistantTriggerKey(
+ uid = entity.uid,
+ type = type,
+ clickType = clickType,
+ )
+ }
+
+ fun toEntity(key: AssistantTriggerKey): AssistantTriggerKeyEntity {
+ val type: String = when (key.type) {
+ AssistantTriggerType.VOICE -> AssistantTriggerKeyEntity.ASSISTANT_TYPE_VOICE
+ AssistantTriggerType.DEVICE -> AssistantTriggerKeyEntity.ASSISTANT_TYPE_DEVICE
+ AssistantTriggerType.ANY -> AssistantTriggerKeyEntity.ASSISTANT_TYPE_ANY
+ }
+
+ val clickType: Int = when (key.clickType) {
+ ClickType.SHORT_PRESS -> TriggerKeyEntity.SHORT_PRESS
+ ClickType.LONG_PRESS -> TriggerKeyEntity.LONG_PRESS
+ ClickType.DOUBLE_PRESS -> TriggerKeyEntity.DOUBLE_PRESS
+ }
+
+ return AssistantTriggerKeyEntity(
+ type = type,
+ clickType = clickType,
+ uid = key.uid,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerType.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerType.kt
new file mode 100644
index 0000000000..f82c6ad9b0
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerType.kt
@@ -0,0 +1,16 @@
+package io.github.sds100.keymapper.mappings.keymaps.trigger
+
+/**
+ * The type of assistant that triggers an assistant trigger key. The voice assistant
+ * is the assistant that handles voice commands. If you press the voice command on a headset or
+ * keyboard then Android asks you which app it should use as default.
+ *
+ * The device assistant is the one selected in the settings as the default for reading on-screen
+ * content and only one app can have this permission at a time. This is the one used when
+ * long-pressing the power button on Pixels and other Android skins.
+ */
+enum class AssistantTriggerType {
+ ANY,
+ VOICE,
+ DEVICE,
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt
similarity index 67%
rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModel.kt
rename to app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt
index 194adcbde1..e9e6f11bcf 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModel.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt
@@ -1,17 +1,16 @@
-package io.github.sds100.keymapper.mappings.keymaps
+package io.github.sds100.keymapper.mappings.keymaps.trigger
import android.os.Build
import android.view.KeyEvent
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.mappings.ClickType
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTrigger
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTriggerError
-import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerState
-import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerUseCase
-import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice
-import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyLinkType
-import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyListItem
-import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode
+import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCase
+import io.github.sds100.keymapper.mappings.keymaps.CreateKeyMapShortcutUseCase
+import io.github.sds100.keymapper.mappings.keymaps.DisplayKeyMapUseCase
+import io.github.sds100.keymapper.mappings.keymaps.KeyMap
import io.github.sds100.keymapper.onboarding.OnboardingUseCase
import io.github.sds100.keymapper.system.devices.InputDeviceUtils
import io.github.sds100.keymapper.system.keyevents.KeyEventUtils
@@ -56,7 +55,7 @@ import kotlinx.coroutines.runBlocking
* Created by sds100 on 24/11/20.
*/
-class ConfigKeyMapTriggerViewModel(
+abstract class BaseConfigTriggerViewModel(
private val coroutineScope: CoroutineScope,
private val onboarding: OnboardingUseCase,
private val config: ConfigKeyMapUseCase,
@@ -68,7 +67,7 @@ class ConfigKeyMapTriggerViewModel(
PopupViewModel by PopupViewModelImpl(),
NavigationViewModel by NavigationViewModelImpl() {
- val optionsViewModel = ConfigKeyMapTriggerOptionsViewModel(
+ val optionsViewModel = ConfigTriggerOptionsViewModel(
coroutineScope,
config,
createKeyMapShortcut,
@@ -82,16 +81,11 @@ class ConfigKeyMapTriggerViewModel(
*/
val openEditOptions = _openEditOptions.asSharedFlow()
- val recordTriggerButtonText: StateFlow = recordTrigger.state.map { recordTriggerState ->
- when (recordTriggerState) {
- is RecordTriggerState.CountingDown -> getString(
- R.string.button_recording_trigger_countdown,
- recordTriggerState.timeLeft,
- )
-
- RecordTriggerState.Stopped -> getString(R.string.button_record_trigger)
- }
- }.flowOn(Dispatchers.Default).stateIn(coroutineScope, SharingStarted.Lazily, "")
+ val recordTriggerState: StateFlow = recordTrigger.state.stateIn(
+ coroutineScope,
+ SharingStarted.Lazily,
+ RecordTriggerState.Stopped,
+ )
val triggerModeButtonsEnabled: StateFlow = config.mapping.map { state ->
when (state) {
@@ -124,12 +118,21 @@ class ConfigKeyMapTriggerViewModel(
}
}.flowOn(Dispatchers.Default).stateIn(coroutineScope, SharingStarted.Eagerly, State.Loading)
+ /**
+ * The click type radio buttons are only visible if there is one key
+ * or there are only key code keys in the trigger. It is not possible to do a long press of
+ * non-key code keys in a parallel trigger.
+ */
val clickTypeRadioButtonsVisible: StateFlow = config.mapping.map { state ->
when (state) {
is State.Data -> {
val trigger = state.data.trigger
- trigger.mode is TriggerMode.Parallel || trigger.keys.size == 1
+ if (trigger.mode is TriggerMode.Parallel) {
+ trigger.keys.all { it is KeyCodeTriggerKey }
+ } else {
+ trigger.keys.size == 1
+ }
}
State.Loading -> false
@@ -143,6 +146,30 @@ class ConfigKeyMapTriggerViewModel(
}
}.flowOn(Dispatchers.Default).stateIn(coroutineScope, SharingStarted.Eagerly, false)
+ /**
+ * Long press is only allowed for triggers that only use key code trigger keys.
+ */
+ val longPressButtonVisible: StateFlow = config.mapping.map { state ->
+ when (state) {
+ is State.Data -> state.data.trigger.keys.all { it is KeyCodeTriggerKey }
+ State.Loading -> false
+ }
+ }.flowOn(Dispatchers.Default).stateIn(coroutineScope, SharingStarted.Eagerly, false)
+
+ /**
+ * Only show the buttons for the trigger mode if keys have been added. The buttons
+ * shouldn't be shown when no trigger is selected because they aren't relevant
+ * for advanced triggers.
+ */
+ val triggerModeRadioButtonsVisible: StateFlow = config.mapping
+ .map { state ->
+ when (state) {
+ is State.Data -> state.data.trigger.keys.isNotEmpty()
+ State.Loading -> false
+ }
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, false)
+
val checkedClickTypeRadioButton: StateFlow = config.mapping.map { state ->
when (state) {
is State.Data -> {
@@ -176,6 +203,8 @@ class ConfigKeyMapTriggerViewModel(
private val _fixAppKilling = MutableSharedFlow()
val fixAppKilling = _fixAppKilling.asSharedFlow()
+ var showAdvancedTriggersBottomSheet: Boolean by mutableStateOf(false)
+
init {
val rebuildErrorList = MutableSharedFlow>(replay = 1)
@@ -222,7 +251,7 @@ class ConfigKeyMapTriggerViewModel(
showPopup("screen_pinning_message", dialog)
}
- config.addTriggerKey(it.keyCode, it.device)
+ config.addKeyCodeTriggerKey(it.keyCode, it.device)
}.launchIn(coroutineScope)
coroutineScope.launch {
@@ -259,23 +288,33 @@ class ConfigKeyMapTriggerViewModel(
}
}
- private fun buildTriggerErrorListItems(triggerErrors: List) =
+ private fun buildTriggerErrorListItems(triggerErrors: List): List =
triggerErrors.map { error ->
when (error) {
- KeyMapTriggerError.DND_ACCESS_DENIED -> TextListItem.Error(
+ TriggerError.DND_ACCESS_DENIED -> TextListItem.Error(
id = error.toString(),
text = getString(R.string.trigger_error_dnd_access_denied),
)
- KeyMapTriggerError.SCREEN_OFF_ROOT_DENIED -> TextListItem.Error(
+ TriggerError.SCREEN_OFF_ROOT_DENIED -> TextListItem.Error(
id = error.toString(),
text = getString(R.string.trigger_error_screen_off_root_permission_denied),
)
- KeyMapTriggerError.CANT_DETECT_IN_PHONE_CALL -> TextListItem.Error(
+ TriggerError.CANT_DETECT_IN_PHONE_CALL -> TextListItem.Error(
id = error.toString(),
text = getString(R.string.trigger_error_cant_detect_in_phone_call),
)
+
+ TriggerError.ASSISTANT_NOT_SELECTED -> TextListItem.Error(
+ id = error.toString(),
+ text = getString(R.string.trigger_error_assistant_activity_not_chosen),
+ )
+
+ TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> TextListItem.Error(
+ id = error.toString(),
+ text = getString(R.string.trigger_error_assistant_not_purchased),
+ )
}
}
@@ -302,48 +341,52 @@ class ConfigKeyMapTriggerViewModel(
fun onRemoveKeyClick(uid: String) = config.removeTriggerKey(uid)
fun onMoveTriggerKey(fromIndex: Int, toIndex: Int) = config.moveTriggerKey(fromIndex, toIndex)
- fun onTriggerKeyOptionsClick(id: String) {
+ open fun onTriggerKeyOptionsClick(id: String) {
runBlocking { _openEditOptions.emit(id) }
}
fun onChooseDeviceClick(keyUid: String) {
coroutineScope.launch {
- val idAny = "any"
- val idInternal = "this_device"
- val devices = config.getAvailableTriggerKeyDevices()
- val showDeviceDescriptors = displayKeyMap.showDeviceDescriptors.first()
-
- val listItems = devices.map { device: TriggerKeyDevice ->
- when (device) {
- TriggerKeyDevice.Any -> idAny to getString(R.string.any_device)
- TriggerKeyDevice.Internal -> idInternal to getString(R.string.this_device)
- is TriggerKeyDevice.External -> {
- if (showDeviceDescriptors) {
- val name = InputDeviceUtils.appendDeviceDescriptorToName(
- device.descriptor,
- device.name,
- )
- device.descriptor to name
- } else {
- device.descriptor to device.name
- }
+ chooseDeviceForKeyCodeTriggerKey(keyUid)
+ }
+ }
+
+ private suspend fun chooseDeviceForKeyCodeTriggerKey(keyUid: String) {
+ val idAny = "any"
+ val idInternal = "this_device"
+ val devices = config.getAvailableTriggerKeyDevices()
+ val showDeviceDescriptors = displayKeyMap.showDeviceDescriptors.first()
+
+ val listItems = devices.map { device: TriggerKeyDevice ->
+ when (device) {
+ TriggerKeyDevice.Any -> idAny to getString(R.string.any_device)
+ TriggerKeyDevice.Internal -> idInternal to getString(R.string.this_device)
+ is TriggerKeyDevice.External -> {
+ if (showDeviceDescriptors) {
+ val name = InputDeviceUtils.appendDeviceDescriptorToName(
+ device.descriptor,
+ device.name,
+ )
+ device.descriptor to name
+ } else {
+ device.descriptor to device.name
}
}
}
+ }
- val triggerKeyDeviceId = showPopup(
- "pick_trigger_key_device",
- PopupUi.SingleChoice(listItems),
- ) ?: return@launch
-
- val selectedTriggerKeyDevice = when (triggerKeyDeviceId) {
- idAny -> TriggerKeyDevice.Any
- idInternal -> TriggerKeyDevice.Internal
- else -> devices.single { it is TriggerKeyDevice.External && it.descriptor == triggerKeyDeviceId }
- }
+ val triggerKeyDeviceId = showPopup(
+ "pick_trigger_key_device",
+ PopupUi.SingleChoice(listItems),
+ ) ?: return
- config.setTriggerKeyDevice(keyUid, selectedTriggerKeyDevice)
+ val selectedTriggerKeyDevice = when (triggerKeyDeviceId) {
+ idAny -> TriggerKeyDevice.Any
+ idInternal -> TriggerKeyDevice.Internal
+ else -> devices.single { it is TriggerKeyDevice.External && it.descriptor == triggerKeyDeviceId }
}
+
+ config.setTriggerKeyDevice(keyUid, selectedTriggerKeyDevice)
}
fun onRecordTriggerButtonClick() {
@@ -357,8 +400,8 @@ class ConfigKeyMapTriggerViewModel(
if (result is Error.AccessibilityServiceDisabled) {
ViewModelHelper.handleAccessibilityServiceStoppedSnackBar(
- resourceProvider = this@ConfigKeyMapTriggerViewModel,
- popupViewModel = this@ConfigKeyMapTriggerViewModel,
+ resourceProvider = this@BaseConfigTriggerViewModel,
+ popupViewModel = this@BaseConfigTriggerViewModel,
startService = displayKeyMap::startAccessibilityService,
message = R.string.dialog_message_enable_accessibility_service_to_record_trigger,
)
@@ -366,8 +409,8 @@ class ConfigKeyMapTriggerViewModel(
if (result is Error.AccessibilityServiceCrashed) {
ViewModelHelper.handleAccessibilityServiceCrashedSnackBar(
- resourceProvider = this@ConfigKeyMapTriggerViewModel,
- popupViewModel = this@ConfigKeyMapTriggerViewModel,
+ resourceProvider = this@BaseConfigTriggerViewModel,
+ popupViewModel = this@BaseConfigTriggerViewModel,
restartService = displayKeyMap::restartAccessibilityService,
message = R.string.dialog_message_restart_accessibility_service_to_record_trigger,
)
@@ -383,42 +426,41 @@ class ConfigKeyMapTriggerViewModel(
fun onTriggerErrorClick(listItemId: String) {
coroutineScope.launch {
- when (KeyMapTriggerError.valueOf(listItemId)) {
- KeyMapTriggerError.DND_ACCESS_DENIED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ when (TriggerError.valueOf(listItemId)) {
+ TriggerError.DND_ACCESS_DENIED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable(
- resourceProvider = this@ConfigKeyMapTriggerViewModel,
- popupViewModel = this@ConfigKeyMapTriggerViewModel,
+ resourceProvider = this@BaseConfigTriggerViewModel,
+ popupViewModel = this@BaseConfigTriggerViewModel,
neverShowDndTriggerErrorAgain = { displayKeyMap.neverShowDndTriggerErrorAgain() },
fixError = { displayKeyMap.fixError(it) },
)
}
- KeyMapTriggerError.SCREEN_OFF_ROOT_DENIED -> {
+ TriggerError.SCREEN_OFF_ROOT_DENIED -> {
val error = Error.PermissionDenied(Permission.ROOT)
displayKeyMap.fixError(error)
}
- KeyMapTriggerError.CANT_DETECT_IN_PHONE_CALL -> {
+ TriggerError.CANT_DETECT_IN_PHONE_CALL -> {
displayKeyMap.fixError(Error.CantDetectKeyEventsInPhoneCall)
}
+
+ TriggerError.ASSISTANT_NOT_SELECTED -> {
+ displayKeyMap.fixError(Error.PermissionDenied(Permission.DEVICE_ASSISTANT))
+ }
+
+ TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> {
+ showAdvancedTriggersBottomSheet = true
+ }
}
}
}
private fun createListItems(
- trigger: KeyMapTrigger,
+ trigger: Trigger,
showDeviceDescriptors: Boolean,
): List =
trigger.keys.mapIndexed { index, key ->
- val extraInfo = buildString {
- append(getTriggerKeyDeviceName(key.device, showDeviceDescriptors))
-
- if (!key.consumeKeyEvent) {
- val midDot = getString(R.string.middot)
- append(" $midDot ${getString(R.string.flag_dont_override_default_action)}")
- }
- }
-
val clickTypeString = when (key.clickType) {
ClickType.SHORT_PRESS -> null
ClickType.LONG_PRESS -> getString(R.string.clicktype_long_press)
@@ -433,15 +475,40 @@ class ConfigKeyMapTriggerViewModel(
TriggerKeyListItem(
id = key.uid,
- keyCode = key.keyCode,
- name = KeyEventUtils.keyCodeToString(key.keyCode),
+ name = getTriggerKeyName(key),
clickTypeString = clickTypeString,
- extraInfo = extraInfo,
+ extraInfo = getTriggerKeyExtraInfo(key, showDeviceDescriptors),
linkType = linkDrawable,
isDragDropEnabled = trigger.keys.size > 1,
+ isChooseDeviceButtonVisible = key is KeyCodeTriggerKey,
)
}
+ private fun getTriggerKeyExtraInfo(key: TriggerKey, showDeviceDescriptors: Boolean): String? {
+ if (key !is KeyCodeTriggerKey) {
+ return null
+ }
+
+ return buildString {
+ append(getTriggerKeyDeviceName(key.device, showDeviceDescriptors))
+
+ if (!key.consumeEvent) {
+ val midDot = getString(R.string.middot)
+ append(" $midDot ${getString(R.string.flag_dont_override_default_action)}")
+ }
+ }
+ }
+
+ private fun getTriggerKeyName(key: TriggerKey): String = when (key) {
+ is AssistantTriggerKey -> when (key.type) {
+ AssistantTriggerType.ANY -> getString(R.string.assistant_any_trigger_name)
+ AssistantTriggerType.VOICE -> getString(R.string.assistant_voice_trigger_name)
+ AssistantTriggerType.DEVICE -> getString(R.string.assistant_device_trigger_name)
+ }
+
+ is KeyCodeTriggerKey -> KeyEventUtils.keyCodeToString(key.keyCode)
+ }
+
private fun getTriggerKeyDeviceName(
device: TriggerKeyDevice,
showDeviceDescriptors: Boolean,
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerKeyViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerKeyViewModel.kt
index 3b3a8542f2..c25a3bff47 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerKeyViewModel.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerKeyViewModel.kt
@@ -84,12 +84,16 @@ class ConfigTriggerKeyViewModel(
triggerKeyUid.value = uid
}
- private fun createListItems(triggerMode: TriggerMode, key: TriggerKey): List =
- sequence {
+ private fun createListItems(triggerMode: TriggerMode, key: TriggerKey): List {
+ if (key !is KeyCodeTriggerKey) {
+ return emptyList()
+ }
+
+ return sequence {
yield(
CheckBoxListItem(
id = ID_DONT_CONSUME_KEY_EVENT,
- isChecked = !key.consumeKeyEvent,
+ isChecked = !key.consumeEvent,
label = getString(R.string.flag_dont_override_default_action),
),
)
@@ -115,4 +119,5 @@ class ConfigTriggerKeyViewModel(
)
}
}.toList()
+ }
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerOptionsFragment.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerOptionsFragment.kt
index 1f8cb7419b..0f6791648e 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerOptionsFragment.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerOptionsFragment.kt
@@ -6,7 +6,6 @@ import androidx.core.content.getSystemService
import androidx.navigation.navGraphViewModels
import com.airbnb.epoxy.EpoxyRecyclerView
import io.github.sds100.keymapper.R
-import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapTriggerOptionsViewModel
import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapViewModel
import io.github.sds100.keymapper.system.url.UrlUtils
import io.github.sds100.keymapper.triggerFromOtherApps
@@ -39,7 +38,7 @@ class ConfigTriggerOptionsFragment : SimpleRecyclerViewFragment() {
Inject.configKeyMapViewModel(requireContext())
}
- private val viewModel: ConfigKeyMapTriggerOptionsViewModel
+ private val viewModel: ConfigTriggerOptionsViewModel
get() = configKeyMapViewModel.configTriggerViewModel.optionsViewModel
override var isAppBarVisible = false
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerOptionsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerOptionsViewModel.kt
similarity index 96%
rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerOptionsViewModel.kt
rename to app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerOptionsViewModel.kt
index d59d6665b2..dd3c3648b7 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerOptionsViewModel.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerOptionsViewModel.kt
@@ -1,8 +1,10 @@
-package io.github.sds100.keymapper.mappings.keymaps
+package io.github.sds100.keymapper.mappings.keymaps.trigger
import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.mappings.OptionMinimums
-import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerFromOtherAppsListItem
+import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCase
+import io.github.sds100.keymapper.mappings.keymaps.CreateKeyMapShortcutUseCase
+import io.github.sds100.keymapper.mappings.keymaps.KeyMap
import io.github.sds100.keymapper.util.Defaultable
import io.github.sds100.keymapper.util.State
import io.github.sds100.keymapper.util.dataOrNull
@@ -32,7 +34,7 @@ import kotlinx.coroutines.withContext
/**
* Created by sds100 on 29/11/20.
*/
-class ConfigKeyMapTriggerOptionsViewModel(
+class ConfigTriggerOptionsViewModel(
private val coroutineScope: CoroutineScope,
private val config: ConfigKeyMapUseCase,
private val createKeyMapShortcut: CreateKeyMapShortcutUseCase,
@@ -109,7 +111,7 @@ class ConfigKeyMapTriggerOptionsViewModel(
result.onFailure { error ->
val snackBar = PopupUi.SnackBar(
- message = error.getFullMessage(this@ConfigKeyMapTriggerOptionsViewModel),
+ message = error.getFullMessage(this@ConfigTriggerOptionsViewModel),
)
showPopup("create_shortcut_result", snackBar)
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt
new file mode 100644
index 0000000000..5c5bdcd926
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt
@@ -0,0 +1,85 @@
+package io.github.sds100.keymapper.mappings.keymaps.trigger
+
+import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity
+import io.github.sds100.keymapper.data.entities.TriggerKeyEntity
+import io.github.sds100.keymapper.mappings.ClickType
+import kotlinx.serialization.Serializable
+import splitties.bitflags.hasFlag
+import splitties.bitflags.withFlag
+import java.util.UUID
+
+@Serializable
+data class KeyCodeTriggerKey(
+ override val uid: String = UUID.randomUUID().toString(),
+ val keyCode: Int,
+ val device: TriggerKeyDevice,
+ override val clickType: ClickType,
+ override val consumeEvent: Boolean = true,
+) : TriggerKey() {
+
+ override fun toString(): String {
+ val deviceString = when (device) {
+ TriggerKeyDevice.Any -> "any"
+ is TriggerKeyDevice.External -> "external"
+ TriggerKeyDevice.Internal -> "internal"
+ }
+ return "KeyCodeTriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeEvent) "
+ }
+
+ companion object {
+ fun fromEntity(entity: KeyCodeTriggerKeyEntity): TriggerKey = KeyCodeTriggerKey(
+ uid = entity.uid,
+ keyCode = entity.keyCode,
+ device = when (entity.deviceId) {
+ KeyCodeTriggerKeyEntity.DEVICE_ID_THIS_DEVICE -> TriggerKeyDevice.Internal
+ KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE -> TriggerKeyDevice.Any
+ else -> TriggerKeyDevice.External(
+ entity.deviceId,
+ entity.deviceName ?: "",
+ )
+ },
+ clickType = when (entity.clickType) {
+ TriggerKeyEntity.SHORT_PRESS -> ClickType.SHORT_PRESS
+ TriggerKeyEntity.LONG_PRESS -> ClickType.LONG_PRESS
+ TriggerKeyEntity.DOUBLE_PRESS -> ClickType.DOUBLE_PRESS
+ else -> ClickType.SHORT_PRESS
+ },
+ consumeEvent = !entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT),
+ )
+
+ fun toEntity(key: KeyCodeTriggerKey): KeyCodeTriggerKeyEntity {
+ val deviceId = when (key.device) {
+ TriggerKeyDevice.Any -> KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE
+ is TriggerKeyDevice.External -> key.device.descriptor
+ TriggerKeyDevice.Internal -> KeyCodeTriggerKeyEntity.DEVICE_ID_THIS_DEVICE
+ }
+
+ val deviceName = if (key.device is TriggerKeyDevice.External) {
+ key.device.name
+ } else {
+ null
+ }
+
+ val clickType = when (key.clickType) {
+ ClickType.SHORT_PRESS -> TriggerKeyEntity.SHORT_PRESS
+ ClickType.LONG_PRESS -> TriggerKeyEntity.LONG_PRESS
+ ClickType.DOUBLE_PRESS -> TriggerKeyEntity.DOUBLE_PRESS
+ }
+
+ var flags = 0
+
+ if (!key.consumeEvent) {
+ flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT)
+ }
+
+ return KeyCodeTriggerKeyEntity(
+ keyCode = key.keyCode,
+ deviceId = deviceId,
+ deviceName = deviceName,
+ clickType = clickType,
+ flags = flags,
+ uid = key.uid,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapTriggerError.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapTriggerError.kt
deleted file mode 100644
index c1a996ef6d..0000000000
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapTriggerError.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package io.github.sds100.keymapper.mappings.keymaps.trigger
-
-/**
- * Created by sds100 on 04/04/2021.
- */
-enum class KeyMapTriggerError {
- DND_ACCESS_DENIED,
- SCREEN_OFF_ROOT_DENIED,
- CANT_DETECT_IN_PHONE_CALL,
-}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerButtonRow.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerButtonRow.kt
new file mode 100644
index 0000000000..1a62fb1585
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerButtonRow.kt
@@ -0,0 +1,179 @@
+package io.github.sds100.keymapper.mappings.keymaps.trigger
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Badge
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.github.sds100.keymapper.R
+import io.github.sds100.keymapper.compose.KeyMapperTheme
+import io.github.sds100.keymapper.compose.LocalCustomColorsPalette
+
+/**
+ * This row of buttons is shown at the bottom of the TriggerFragment.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun RecordTriggerButtonRow(
+ modifier: Modifier = Modifier,
+ viewModel: ConfigTriggerViewModel,
+) {
+ val recordTriggerState by viewModel.recordTriggerState.collectAsState()
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+
+ if (viewModel.showAdvancedTriggersBottomSheet) {
+ AdvancedTriggersBottomSheet(
+ modifier = Modifier.systemBarsPadding(),
+ viewModel = viewModel,
+ onDismissRequest = {
+ viewModel.showAdvancedTriggersBottomSheet = false
+ },
+ sheetState = sheetState,
+ )
+ }
+
+ RecordTriggerButtonRow(
+ modifier = modifier,
+ onRecordTriggerClick = viewModel::onRecordTriggerButtonClick,
+ recordTriggerState = recordTriggerState,
+ onAdvancedTriggersClick = {
+ viewModel.showAdvancedTriggersBottomSheet = true
+ },
+ )
+}
+
+/**
+ * This row of buttons is shown at the bottom of the TriggerFragment.
+ */
+@Composable
+private fun RecordTriggerButtonRow(
+ modifier: Modifier = Modifier,
+ onRecordTriggerClick: () -> Unit = {},
+ recordTriggerState: RecordTriggerState,
+ onAdvancedTriggersClick: () -> Unit = {},
+) {
+ Row(modifier) {
+ RecordTriggerButton(
+ modifier = Modifier
+ .weight(1f)
+ .align(Alignment.Bottom),
+ recordTriggerState,
+ onClick = onRecordTriggerClick,
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ AdvancedTriggersButton(
+ modifier = Modifier.weight(1f),
+ isEnabled = recordTriggerState is RecordTriggerState.Stopped,
+ onClick = onAdvancedTriggersClick,
+ )
+ }
+}
+
+@Composable
+private fun RecordTriggerButton(
+ modifier: Modifier,
+ state: RecordTriggerState,
+ onClick: () -> Unit,
+) {
+ val colors = ButtonDefaults.filledTonalButtonColors().copy(
+ containerColor = LocalCustomColorsPalette.current.red,
+ contentColor = LocalCustomColorsPalette.current.onRed,
+ )
+
+ val text: String = when (state) {
+ is RecordTriggerState.CountingDown ->
+ stringResource(R.string.button_recording_trigger_countdown, state.timeLeft)
+
+ RecordTriggerState.Stopped ->
+ stringResource(R.string.button_record_trigger)
+ }
+
+ FilledTonalButton(
+ modifier = modifier,
+ onClick = onClick,
+ colors = colors,
+ ) {
+ Text(text)
+ }
+}
+
+@Composable
+private fun AdvancedTriggersButton(
+ modifier: Modifier,
+ isEnabled: Boolean,
+ onClick: () -> Unit,
+) {
+ Box(modifier = modifier) {
+ OutlinedButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 20.dp),
+ enabled = isEnabled,
+ onClick = onClick,
+ ) {
+ Text(stringResource(R.string.button_advanced_triggers))
+ }
+
+ Badge(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .height(36.dp),
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 8.dp),
+ text = stringResource(R.string.button_advanced_triggers_badge),
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+}
+
+@Preview(widthDp = 400)
+@Composable
+private fun PreviewCountingDown() {
+ KeyMapperTheme {
+ Surface {
+ RecordTriggerButtonRow(
+ modifier = Modifier.fillMaxWidth(),
+ recordTriggerState = RecordTriggerState.CountingDown(3),
+ )
+ }
+ }
+}
+
+@Preview(widthDp = 400)
+@Composable
+private fun PreviewStopped() {
+ KeyMapperTheme {
+ Surface {
+ RecordTriggerButtonRow(
+ modifier = Modifier.fillMaxWidth(),
+ recordTriggerState = RecordTriggerState.Stopped,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapTrigger.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt
similarity index 87%
rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapTrigger.kt
rename to app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt
index e00454c0df..0bc2f1f99c 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapTrigger.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt
@@ -15,7 +15,7 @@ import splitties.bitflags.withFlag
*/
@Serializable
-data class KeyMapTrigger(
+data class Trigger(
val keys: List = emptyList(),
val mode: TriggerMode = TriggerMode.Undefined,
val vibrate: Boolean = false,
@@ -42,20 +42,27 @@ data class KeyMapTrigger(
(keys.size == 1 || (mode is TriggerMode.Parallel)) &&
keys.getOrNull(0)?.clickType == ClickType.LONG_PRESS
- fun isDetectingWhenScreenOffAllowed(): Boolean = keys.isNotEmpty() &&
- keys.all {
- KeyEventUtils.canDetectKeyWhenScreenOff(it.keyCode)
- }
+ /**
+ * Must check that it is not empty otherwise it would be true from the "all" check.
+ * It is not allowed if the key is an assistant button because it is assumed to be true
+ * anyway.
+ */
+ fun isDetectingWhenScreenOffAllowed(): Boolean {
+ return keys.isNotEmpty() &&
+ keys.all {
+ it is KeyCodeTriggerKey && KeyEventUtils.canDetectKeyWhenScreenOff(it.keyCode)
+ }
+ }
fun isChangingSequenceTriggerTimeoutAllowed(): Boolean =
- !keys.isNullOrEmpty() && keys.size > 1 && mode is TriggerMode.Sequence
+ keys.isNotEmpty() && keys.size > 1 && mode is TriggerMode.Sequence
}
-object KeymapTriggerEntityMapper {
+object TriggerEntityMapper {
fun fromEntity(
entity: TriggerEntity,
- ): KeyMapTrigger {
- val keys = entity.keys.map { KeymapTriggerKeyEntityMapper.fromEntity(it) }
+ ): Trigger {
+ val keys = entity.keys.map { TriggerKey.fromEntity(it) }
val mode = when {
entity.mode == TriggerEntity.SEQUENCE && keys.size > 1 -> TriggerMode.Sequence
@@ -63,7 +70,7 @@ object KeymapTriggerEntityMapper {
else -> TriggerMode.Undefined
}
- return KeyMapTrigger(
+ return Trigger(
keys = keys,
mode = mode,
@@ -90,7 +97,7 @@ object KeymapTriggerEntityMapper {
)
}
- fun toEntity(trigger: KeyMapTrigger): TriggerEntity {
+ fun toEntity(trigger: Trigger): TriggerEntity {
val extras = mutableListOf()
if (trigger.isChangingSequenceTriggerTimeoutAllowed() && trigger.sequenceTriggerTimeout != null) {
@@ -158,7 +165,7 @@ object KeymapTriggerEntityMapper {
}
return TriggerEntity(
- keys = trigger.keys.map { KeymapTriggerKeyEntityMapper.toEntity(it) },
+ keys = trigger.keys.map { TriggerKey.toEntity(it) },
extras = extras,
mode = mode,
flags = flags,
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt
new file mode 100644
index 0000000000..13a35842a5
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt
@@ -0,0 +1,18 @@
+package io.github.sds100.keymapper.mappings.keymaps.trigger
+
+/**
+ * Created by sds100 on 04/04/2021.
+ */
+enum class TriggerError {
+ DND_ACCESS_DENIED,
+ SCREEN_OFF_ROOT_DENIED,
+ CANT_DETECT_IN_PHONE_CALL,
+
+ // Key Mapper is not selected as the assistant activity. This is required for assistant
+ // triggers.
+ ASSISTANT_NOT_SELECTED,
+
+ // This error appears when a key map has an assistant trigger but the user hasn't purchased
+ // the product.
+ ASSISTANT_TRIGGER_NOT_PURCHASED,
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerFragment.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerFragment.kt
index 0b1fd3b40e..9564b287e7 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerFragment.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerFragment.kt
@@ -3,6 +3,9 @@ package io.github.sds100.keymapper.mappings.keymaps.trigger
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.Lifecycle
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.ItemTouchHelper
@@ -12,15 +15,14 @@ import com.airbnb.epoxy.EpoxyTouchHelper
import com.google.android.material.card.MaterialCardView
import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.TriggerKeyBindingModel_
+import io.github.sds100.keymapper.compose.KeyMapperTheme
import io.github.sds100.keymapper.databinding.FragmentTriggerBinding
import io.github.sds100.keymapper.fixError
-import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapTriggerViewModel
import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapViewModel
import io.github.sds100.keymapper.triggerKey
import io.github.sds100.keymapper.util.FragmentInfo
import io.github.sds100.keymapper.util.Inject
import io.github.sds100.keymapper.util.State
-import io.github.sds100.keymapper.util.color
import io.github.sds100.keymapper.util.launchRepeatOnLifecycle
import io.github.sds100.keymapper.util.ui.RecyclerViewFragment
import kotlinx.coroutines.flow.Flow
@@ -39,7 +41,7 @@ class TriggerFragment : RecyclerViewFragment(R.id.nav_config_keymap) {
Inject.configKeyMapViewModel(requireContext())
}.value.configTriggerViewModel
@@ -48,22 +50,31 @@ class TriggerFragment : RecyclerViewFragment>>
- get() = configKeyMapTriggerViewModel.triggerKeyListItems
+ get() = configTriggerViewModel.triggerKeyListItems
override fun bind(inflater: LayoutInflater, container: ViewGroup?) =
FragmentTriggerBinding.inflate(inflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
+
+ composeViewRecordTriggerButtons.apply {
+ // Dispose of the Composition when the view's LifecycleOwner
+ // is destroyed
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ KeyMapperTheme {
+ RecordTriggerButtonRow(Modifier.fillMaxWidth(), configTriggerViewModel)
+ }
+ }
+ }
}
override fun subscribeUi(binding: FragmentTriggerBinding) {
- binding.viewModel = configKeyMapTriggerViewModel
-
- binding.buttonRecordKeys.setBackgroundColor(color(R.color.red, harmonize = true))
+ binding.viewModel = configTriggerViewModel
binding.recyclerViewTriggerKeys.adapter = triggerKeyController.adapter
viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) {
- configKeyMapTriggerViewModel.errorListItems.collectLatest { listItems ->
+ configTriggerViewModel.errorListItems.collectLatest { listItems ->
binding.enableTriggerKeyDragging(triggerKeyController)
@@ -74,7 +85,7 @@ class TriggerFragment : RecyclerViewFragment
- configKeyMapTriggerViewModel.onTriggerErrorClick(it.id)
+ configTriggerViewModel.onTriggerErrorClick(it.id)
}
}
}
@@ -98,7 +109,7 @@ class TriggerFragment : RecyclerViewFragment
- configKeyMapTriggerViewModel.onRemoveKeyClick(model.id)
+ configTriggerViewModel.onRemoveKeyClick(model.id)
}
onMoreClick { _ ->
- configKeyMapTriggerViewModel.onTriggerKeyOptionsClick(model.id)
+ configTriggerViewModel.onTriggerKeyOptionsClick(model.id)
}
onDeviceClick { _ ->
- configKeyMapTriggerViewModel.onChooseDeviceClick(model.id)
+ configTriggerViewModel.onChooseDeviceClick(model.id)
}
}
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt
index d437859aa5..27a5d49b51 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt
@@ -1,90 +1,37 @@
package io.github.sds100.keymapper.mappings.keymaps.trigger
-import io.github.sds100.keymapper.data.entities.TriggerEntity
+import io.github.sds100.keymapper.data.entities.AssistantTriggerKeyEntity
+import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity
+import io.github.sds100.keymapper.data.entities.TriggerKeyEntity
import io.github.sds100.keymapper.mappings.ClickType
import kotlinx.serialization.Serializable
-import splitties.bitflags.hasFlag
-import splitties.bitflags.withFlag
-import java.util.UUID
-/**
- * Created by sds100 on 21/02/2021.
- */
@Serializable
-data class TriggerKey(
- val uid: String = UUID.randomUUID().toString(),
- val keyCode: Int,
- val device: TriggerKeyDevice,
- val clickType: ClickType,
-
- val consumeKeyEvent: Boolean = true,
-) {
-
- override fun toString(): String {
- val deviceString = when (device) {
- TriggerKeyDevice.Any -> "any"
- is TriggerKeyDevice.External -> "external"
- TriggerKeyDevice.Internal -> "internal"
- }
- return "TriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeKeyEvent) "
- }
-}
-
-object KeymapTriggerKeyEntityMapper {
- fun fromEntity(
- entity: TriggerEntity.KeyEntity,
- ): TriggerKey = TriggerKey(
- uid = entity.uid,
- keyCode = entity.keyCode,
- device = when (entity.deviceId) {
- TriggerEntity.KeyEntity.DEVICE_ID_THIS_DEVICE -> TriggerKeyDevice.Internal
- TriggerEntity.KeyEntity.DEVICE_ID_ANY_DEVICE -> TriggerKeyDevice.Any
- else -> TriggerKeyDevice.External(
- entity.deviceId,
- entity.deviceName ?: "",
- )
- },
- clickType = when (entity.clickType) {
- TriggerEntity.SHORT_PRESS -> ClickType.SHORT_PRESS
- TriggerEntity.LONG_PRESS -> ClickType.LONG_PRESS
- TriggerEntity.DOUBLE_PRESS -> ClickType.DOUBLE_PRESS
- else -> ClickType.SHORT_PRESS
- },
- consumeKeyEvent = !entity.flags.hasFlag(TriggerEntity.KeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT),
- )
-
- fun toEntity(key: TriggerKey): TriggerEntity.KeyEntity {
- val deviceId = when (key.device) {
- TriggerKeyDevice.Any -> TriggerEntity.KeyEntity.DEVICE_ID_ANY_DEVICE
- is TriggerKeyDevice.External -> key.device.descriptor
- TriggerKeyDevice.Internal -> TriggerEntity.KeyEntity.DEVICE_ID_THIS_DEVICE
+sealed class TriggerKey {
+ abstract val clickType: ClickType
+
+ /**
+ * Whether the event that triggers this key will be consumed and not passed
+ * onto subsequent apps. E.g consuming the volume down key event will mean the volume
+ * doesn't change.
+ */
+ abstract val consumeEvent: Boolean
+ abstract val uid: String
+
+ companion object {
+ fun fromEntity(entity: TriggerKeyEntity): TriggerKey = when (entity) {
+ is AssistantTriggerKeyEntity -> AssistantTriggerKey.fromEntity(entity)
+ is KeyCodeTriggerKeyEntity -> KeyCodeTriggerKey.fromEntity(entity)
}
- val deviceName = if (key.device is TriggerKeyDevice.External) {
- key.device.name
- } else {
- null
- }
-
- val clickType = when (key.clickType) {
- ClickType.SHORT_PRESS -> TriggerEntity.SHORT_PRESS
- ClickType.LONG_PRESS -> TriggerEntity.LONG_PRESS
- ClickType.DOUBLE_PRESS -> TriggerEntity.DOUBLE_PRESS
- }
-
- var flags = 0
-
- if (!key.consumeKeyEvent) {
- flags = flags.withFlag(TriggerEntity.KeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT)
+ fun toEntity(key: TriggerKey): TriggerKeyEntity = when (key) {
+ is AssistantTriggerKey -> AssistantTriggerKey.toEntity(key)
+ is KeyCodeTriggerKey -> KeyCodeTriggerKey.toEntity(key)
}
+ }
- return TriggerEntity.KeyEntity(
- keyCode = key.keyCode,
- deviceId = deviceId,
- deviceName = deviceName,
- clickType = clickType,
- flags = flags,
- uid = key.uid,
- )
+ fun setClickType(clickType: ClickType): TriggerKey = when (this) {
+ is AssistantTriggerKey -> copy(clickType = clickType)
+ is KeyCodeTriggerKey -> copy(clickType = clickType)
}
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt
index cb53febc84..0c303265c7 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt
@@ -2,10 +2,6 @@ package io.github.sds100.keymapper.mappings.keymaps.trigger
import kotlinx.serialization.Serializable
-/**
- * Created by sds100 on 21/02/2021.
- */
-
@Serializable
sealed class TriggerKeyDevice {
@Serializable
@@ -16,4 +12,12 @@ sealed class TriggerKeyDevice {
@Serializable
data class External(val descriptor: String, val name: String) : TriggerKeyDevice()
+
+ fun isSameDevice(other: TriggerKeyDevice): Boolean {
+ if (other is External && this is External) {
+ return other.descriptor == this.descriptor
+ } else {
+ return true
+ }
+ }
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt
index b741580786..736d722609 100644
--- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt
@@ -5,7 +5,6 @@ package io.github.sds100.keymapper.mappings.keymaps.trigger
*/
data class TriggerKeyListItem(
val id: String,
- val keyCode: Int,
val name: String,
/**
@@ -18,4 +17,9 @@ data class TriggerKeyListItem(
val linkType: TriggerKeyLinkType,
val isDragDropEnabled: Boolean,
+
+ /**
+ * The button for choosing the device is only visible for key code trigger keys.
+ */
+ val isChooseDeviceButtonVisible: Boolean,
)
diff --git a/app/src/main/java/io/github/sds100/keymapper/onboarding/AppIntroUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/onboarding/AppIntroUseCase.kt
index 19b92f2d1f..1721fc4e46 100644
--- a/app/src/main/java/io/github/sds100/keymapper/onboarding/AppIntroUseCase.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/onboarding/AppIntroUseCase.kt
@@ -51,8 +51,6 @@ class AppIntroUseCaseImpl(
}
override fun shownAppIntro() {
- preferenceRepository.set(Keys.approvedFingerprintFeaturePrompt, true)
- preferenceRepository.set(Keys.approvedSetupChosenDevicesAgain, true)
preferenceRepository.set(Keys.shownAppIntro, true)
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingUseCase.kt
index 8e69bfc3aa..fe3553a583 100644
--- a/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingUseCase.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingUseCase.kt
@@ -1,6 +1,5 @@
package io.github.sds100.keymapper.onboarding
-import androidx.datastore.preferences.core.stringSetPreferencesKey
import io.github.sds100.keymapper.Constants
import io.github.sds100.keymapper.actions.ActionData
import io.github.sds100.keymapper.actions.canUseImeToPerform
@@ -59,11 +58,6 @@ class OnboardingUseCaseImpl(
preferenceRepository.set(Keys.acknowledgedGuiKeyboard, true)
}
- override var approvedFingerprintFeaturePrompt by PrefDelegate(
- Keys.approvedFingerprintFeaturePrompt,
- false,
- )
-
override var shownParallelTriggerOrderExplanation by PrefDelegate(
Keys.shownParallelTriggerOrderExplanation,
false,
@@ -74,7 +68,7 @@ class OnboardingUseCaseImpl(
)
override val showWhatsNew = get(Keys.lastInstalledVersionCodeHomeScreen)
- .map { it ?: -1 < Constants.VERSION_CODE }
+ .map { (it ?: -1) < Constants.VERSION_CODE }
override fun showedWhatsNew() {
set(Keys.lastInstalledVersionCodeHomeScreen, Constants.VERSION_CODE)
@@ -85,67 +79,30 @@ class OnboardingUseCaseImpl(
readText()
}
- override val showFingerprintFeatureNotificationIfAvailable: Flow by lazy {
+ override var approvedAssistantTriggerFeaturePrompt by PrefDelegate(
+ Keys.approvedAssistantTriggerFeaturePrompt,
+ false,
+ )
+
+ /**
+ * Show the assistant trigger only when they *upgrade* to the new version and after they've
+ * completed the app intro, which asks them whether they want to receive notifications.
+ */
+ override val showAssistantTriggerFeatureNotification: Flow =
combine(
get(Keys.lastInstalledVersionCodeBackground).map { it ?: -1 },
- showWhatsNew,
- get(Keys.approvedFingerprintFeaturePrompt).map { it ?: false },
get(Keys.shownAppIntro).map { it ?: false },
- ) { oldVersionCode, showWhatsNew, approvedPrompt, shownAppIntro ->
- // has the user opened the app and will have already seen that they can remap fingerprint gestures
- val handledUpdateInHomeScreen = !showWhatsNew
-
- oldVersionCode < VersionHelper.FINGERPRINT_GESTURES_MIN_VERSION &&
- !handledUpdateInHomeScreen &&
- !approvedPrompt &&
- shownAppIntro
+ get(Keys.approvedAssistantTriggerFeaturePrompt).map { it ?: false },
+ ) { oldVersionCode, shownAppIntro, approvedPrompt ->
+ oldVersionCode < VersionHelper.ASSISTANT_TRIGGER_MIN_VERSION &&
+ shownAppIntro &&
+ !approvedPrompt
}
- }
- override fun showedFingerprintFeatureNotificationIfAvailable() {
+ override fun showedAssistantTriggerFeatureNotification() {
set(Keys.lastInstalledVersionCodeBackground, Constants.VERSION_CODE)
}
- override val showSetupChosenDevicesAgainNotification: Flow =
- get(Keys.approvedSetupChosenDevicesAgain).map { it ?: false }.map { approvedPreviously ->
- val bluetoothDevicesThatShowImePicker =
- get(stringSetPreferencesKey("pref_bluetooth_devices_show_ime_picker")).first()
- ?: emptySet()
-
- val bluetoothDevicesThatChangeIme =
- get(stringSetPreferencesKey("pref_bluetooth_devices")).first() ?: emptySet()
-
- val previouslyChoseBluetoothDevices =
- bluetoothDevicesThatShowImePicker.isNotEmpty() || bluetoothDevicesThatChangeIme.isNotEmpty()
-
- return@map !approvedPreviously && previouslyChoseBluetoothDevices
- }
-
- override fun approvedSetupChosenDevicesAgainNotification() {
- set(Keys.approvedSetupChosenDevicesAgain, true)
- }
-
- override val showSetupChosenDevicesAgainAppIntro: Flow =
- get(Keys.approvedSetupChosenDevicesAgain).map { it ?: false }.map { approvedPreviously ->
-
- val bluetoothDevicesThatShowImePicker =
- get(stringSetPreferencesKey("pref_bluetooth_devices_show_ime_picker")).first()
- ?: emptySet()
-
- val bluetoothDevicesThatChangeIme =
- get(stringSetPreferencesKey("pref_bluetooth_devices")).first()
- ?: emptySet()
-
- val previouslyChoseBluetoothDevices =
- bluetoothDevicesThatShowImePicker.isNotEmpty() || bluetoothDevicesThatChangeIme.isNotEmpty()
-
- return@map !approvedPreviously && previouslyChoseBluetoothDevices
- }
-
- override fun approvedSetupChosenDevicesAgainAppIntro() {
- set(Keys.approvedSetupChosenDevicesAgain, true)
- }
-
override val showQuickStartGuideHint: Flow = get(Keys.shownQuickStartGuideHint).map {
if (it == null) {
true
@@ -194,18 +151,12 @@ interface OnboardingUseCase {
fun isTvDevice(): Boolean
fun neverShowGuiKeyboardPromptsAgain()
- var approvedFingerprintFeaturePrompt: Boolean
var shownParallelTriggerOrderExplanation: Boolean
var shownSequenceTriggerExplanation: Boolean
- val showFingerprintFeatureNotificationIfAvailable: Flow
- fun showedFingerprintFeatureNotificationIfAvailable()
-
- val showSetupChosenDevicesAgainNotification: Flow
- fun approvedSetupChosenDevicesAgainNotification()
-
- val showSetupChosenDevicesAgainAppIntro: Flow
- fun approvedSetupChosenDevicesAgainAppIntro()
+ val showAssistantTriggerFeatureNotification: Flow
+ fun showedAssistantTriggerFeatureNotification()
+ var approvedAssistantTriggerFeaturePrompt: Boolean
val showWhatsNew: Flow
fun showedWhatsNew()
diff --git a/app/src/main/java/io/github/sds100/keymapper/purchasing/ProductId.kt b/app/src/main/java/io/github/sds100/keymapper/purchasing/ProductId.kt
new file mode 100644
index 0000000000..f7235a961c
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/purchasing/ProductId.kt
@@ -0,0 +1,5 @@
+package io.github.sds100.keymapper.purchasing
+
+enum class ProductId(val packageId: String, val entitlementId: String) {
+ ASSISTANT_TRIGGER("assistant_trigger", "assistant_trigger"),
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/purchasing/PurchasingManager.kt b/app/src/main/java/io/github/sds100/keymapper/purchasing/PurchasingManager.kt
new file mode 100644
index 0000000000..4325142743
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/purchasing/PurchasingManager.kt
@@ -0,0 +1,11 @@
+package io.github.sds100.keymapper.purchasing
+
+import io.github.sds100.keymapper.util.Result
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+interface PurchasingManager {
+ val onCompleteProductPurchase: MutableSharedFlow
+ suspend fun launchPurchasingFlow(product: ProductId): Result
+ suspend fun getProductPrice(product: ProductId): Result
+ suspend fun isPurchased(product: ProductId): Result
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceAdapter.kt
index f97fb10254..b80d8c1343 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceAdapter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceAdapter.kt
@@ -77,6 +77,16 @@ class AccessibilityServiceAdapter(
}.launchIn(coroutineScope)
}
+ /**
+ * Send an event to the accessibility service asynchronously. This method
+ * will return immediately and you won't be notified of whether it is sent.
+ */
+ fun sendAsync(event: ServiceEvent) {
+ coroutineScope.launch {
+ eventsToService.emit(event)
+ }
+ }
+
override suspend fun send(event: ServiceEvent): Result<*> {
state.value = getState()
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt
similarity index 96%
rename from app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt
rename to app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt
index 9b3775514b..fb4267bae4 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt
@@ -15,10 +15,10 @@ import io.github.sds100.keymapper.mappings.PauseMappingsUseCase
import io.github.sds100.keymapper.mappings.fingerprintmaps.DetectFingerprintMapsUseCase
import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintGestureMapController
import io.github.sds100.keymapper.mappings.fingerprintmaps.FingerprintMapId
-import io.github.sds100.keymapper.mappings.keymaps.TriggerKeyMapFromOtherAppsController
import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase
import io.github.sds100.keymapper.mappings.keymaps.detection.DetectScreenOffKeyEventsController
import io.github.sds100.keymapper.mappings.keymaps.detection.KeyMapController
+import io.github.sds100.keymapper.mappings.keymaps.detection.TriggerKeyMapFromOtherAppsController
import io.github.sds100.keymapper.reroutekeyevents.RerouteKeyEventsController
import io.github.sds100.keymapper.reroutekeyevents.RerouteKeyEventsUseCase
import io.github.sds100.keymapper.system.devices.DevicesAdapter
@@ -54,7 +54,7 @@ import timber.log.Timber
/**
* Created by sds100 on 17/04/2021.
*/
-class AccessibilityServiceController(
+abstract class BaseAccessibilityServiceController(
private val coroutineScope: CoroutineScope,
private val accessibilityService: IAccessibilityService,
private val inputEvents: SharedFlow,
@@ -92,7 +92,7 @@ class AccessibilityServiceController(
detectConstraintsUseCase,
)
- private val keymapDetectionDelegate = KeyMapController(
+ val keyMapController = KeyMapController(
coroutineScope,
detectKeyMapsUseCase,
performActionsUseCase,
@@ -108,7 +108,7 @@ class AccessibilityServiceController(
private val recordingTrigger: Boolean
get() = recordingTriggerJob != null && recordingTriggerJob?.isActive == true
- private val isPaused: StateFlow = pauseMappingsUseCase.isPaused
+ val isPaused: StateFlow = pauseMappingsUseCase.isPaused
.stateIn(coroutineScope, SharingStarted.Eagerly, false)
private val screenOffTriggersEnabled: StateFlow =
@@ -123,7 +123,7 @@ class AccessibilityServiceController(
if (!isPaused.value) {
withContext(Dispatchers.Main.immediate) {
- keymapDetectionDelegate.onKeyEvent(
+ keyMapController.onKeyEvent(
keyCode,
action,
metaState = 0,
@@ -196,7 +196,7 @@ class AccessibilityServiceController(
}
pauseMappingsUseCase.isPaused.distinctUntilChanged().onEach {
- keymapDetectionDelegate.reset()
+ keyMapController.reset()
fingerprintMapController.reset()
triggerKeyMapFromOtherAppsController.reset()
}.launchIn(coroutineScope)
@@ -320,11 +320,16 @@ class AccessibilityServiceController(
return true
}
- if (!isPaused.value) {
+ if (isPaused.value) {
+ when (action) {
+ KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(keyCode)} - not filtering because paused, $detailedLogInfo")
+ KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(keyCode)} - not filtering because paused, $detailedLogInfo")
+ }
+ } else {
try {
var consume: Boolean
- consume = keymapDetectionDelegate.onKeyEvent(
+ consume = keyMapController.onKeyEvent(
keyCode,
action,
metaState,
@@ -351,11 +356,6 @@ class AccessibilityServiceController(
} catch (e: Exception) {
Timber.e(e)
}
- } else {
- when (action) {
- KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(keyCode)} - not filtering because paused, $detailedLogInfo")
- KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(keyCode)} - not filtering because paused, $detailedLogInfo")
- }
}
return false
@@ -426,7 +426,7 @@ class AccessibilityServiceController(
triggerKeyMapFromOtherAppsController.onDetected(uid)
}
- private fun onEventFromUi(event: ServiceEvent) {
+ open fun onEventFromUi(event: ServiceEvent) {
Timber.d("Service received event from UI: $event")
when (event) {
is ServiceEvent.StartRecordingTrigger ->
@@ -449,7 +449,10 @@ class AccessibilityServiceController(
is ServiceEvent.TestAction -> performActionsUseCase.perform(event.action)
- is ServiceEvent.Ping -> coroutineScope.launch { outputEvents.emit(ServiceEvent.Pong(event.key)) }
+ is ServiceEvent.Ping -> coroutineScope.launch {
+ outputEvents.emit(ServiceEvent.Pong(event.key))
+ }
+
is ServiceEvent.HideKeyboard -> accessibilityService.hideKeyboard()
is ServiceEvent.ShowKeyboard -> accessibilityService.showKeyboard()
is ServiceEvent.ChangeIme -> accessibilityService.switchIme(event.imeId)
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/ControlAccessibilityServiceUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/ControlAccessibilityServiceUseCase.kt
index eaf30321a4..406bd1f7c4 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/ControlAccessibilityServiceUseCase.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/ControlAccessibilityServiceUseCase.kt
@@ -1,5 +1,7 @@
package io.github.sds100.keymapper.system.accessibility
+import io.github.sds100.keymapper.system.permissions.Permission
+import io.github.sds100.keymapper.system.permissions.PermissionAdapter
import kotlinx.coroutines.flow.Flow
/**
@@ -8,6 +10,7 @@ import kotlinx.coroutines.flow.Flow
class ControlAccessibilityServiceUseCaseImpl(
private val adapter: ServiceAdapter,
+ private val permissionAdapter: PermissionAdapter,
) : ControlAccessibilityServiceUseCase {
override val serviceState: Flow = adapter.state
@@ -26,6 +29,13 @@ class ControlAccessibilityServiceUseCaseImpl(
override fun stopService() {
adapter.stop()
}
+
+ /**
+ * @return whether the user must manually start/stop the service.
+ */
+ override fun isUserInteractionRequired(): Boolean {
+ return !permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)
+ }
}
interface ControlAccessibilityServiceUseCase {
@@ -33,4 +43,5 @@ interface ControlAccessibilityServiceUseCase {
fun startService(): Boolean
fun restartService(): Boolean
fun stopService()
+ fun isUserInteractionRequired(): Boolean
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt
index 486dbf0a45..db86d7bb52 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt
@@ -110,8 +110,12 @@ class AndroidAppShortcutAdapter(context: Context) : AppShortcutAdapter {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
- val pendingIntent =
- PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ // See issue #1222 and #1307. Must have FLAG_UPDATE_CURRENT so that
+ // the intent data is updated. If you don't do this and have two app shortcut actions
+ // from the same app then the data isn't updated and both actions will send
+ // the pending intent for the shortcut that was triggered first.
+ val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ val pendingIntent = PendingIntent.getActivity(ctx, 0, intent, flags)
pendingIntent.send()
return Success(Unit)
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt
index 2a5870eede..98059ce108 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt
@@ -136,6 +136,17 @@ class AndroidPackageManagerAdapter(
}
}
+ override fun getDeviceAssistantPackage(): Result {
+ val settingValue = Settings.Secure.getString(ctx.contentResolver, "assistant")
+
+ if (settingValue.isNullOrEmpty()) {
+ return Error.NoDeviceAssistant
+ }
+
+ val packageName = settingValue.split("/").first()
+ return Success(packageName)
+ }
+
override fun enableApp(packageName: String) {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:$packageName")
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageManagerAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageManagerAdapter.kt
index 87036708d9..6d0b7982c8 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageManagerAdapter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/apps/PackageManagerAdapter.kt
@@ -25,6 +25,7 @@ interface PackageManagerAdapter {
fun launchVoiceAssistant(): Result<*>
fun launchDeviceAssistant(): Result<*>
fun isVoiceAssistantInstalled(): Boolean
+ fun getDeviceAssistantPackage(): Result
fun launchCameraApp(): Result<*>
fun launchSettingsApp(): Result<*>
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceInfo.kt b/app/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceInfo.kt
index b2f70596f6..465c3b5adf 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceInfo.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceInfo.kt
@@ -1,7 +1,7 @@
package io.github.sds100.keymapper.system.devices
import android.os.Parcelable
-import kotlinx.android.parcel.Parcelize
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt
index 90923e3ac6..f122db133d 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt
@@ -13,12 +13,19 @@ import io.github.sds100.keymapper.system.SettingsUtils
import io.github.sds100.keymapper.util.Error
import io.github.sds100.keymapper.util.Result
import io.github.sds100.keymapper.util.Success
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
/**
* Created by sds100 on 17/04/2021.
*/
-class AndroidDisplayAdapter(context: Context) : DisplayAdapter {
+class AndroidDisplayAdapter(
+ context: Context,
+ private val coroutineScope: CoroutineScope,
+) : DisplayAdapter {
companion object {
/**
@@ -35,8 +42,18 @@ class AndroidDisplayAdapter(context: Context) : DisplayAdapter {
context ?: return
when (intent.action) {
- Intent.ACTION_SCREEN_ON -> isScreenOn.value = true
- Intent.ACTION_SCREEN_OFF -> isScreenOn.value = false
+ Intent.ACTION_SCREEN_ON -> {
+ // This intent is received before the assistant activity is launched so
+ // wait 100ms before updating. This is so that one can use the screen-off
+ // constraint with the assistant triggers.
+ coroutineScope.launch {
+ delay(100)
+
+ isScreenOn.update { true }
+ }
+ }
+
+ Intent.ACTION_SCREEN_OFF -> isScreenOn.update { false }
}
}
}
@@ -44,6 +61,7 @@ class AndroidDisplayAdapter(context: Context) : DisplayAdapter {
override val isScreenOn = MutableStateFlow(true)
private val displayManager: DisplayManager = ctx.getSystemService()!!
+ override var orientation: Orientation = getDisplayOrientation()
init {
displayManager.registerDisplayListener(
@@ -62,11 +80,7 @@ class AndroidDisplayAdapter(context: Context) : DisplayAdapter {
},
null,
)
- }
- override var orientation: Orientation = getDisplayOrientation()
-
- init {
val filter = IntentFilter()
filter.addAction(Intent.ACTION_SCREEN_ON)
filter.addAction(Intent.ACTION_SCREEN_OFF)
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt
index a0e1f0ccde..61ec51ce61 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeMessenger.kt
@@ -168,13 +168,17 @@ class KeyMapperImeMessengerImpl(
val chars = text.toCharArray(startIndex = 0, endIndex = 1)
- val events: Array = keyCharacterMap.getEvents(chars)
+ val events: Array? = keyCharacterMap.getEvents(chars)
- for (i in events.indices) {
- keyEventRelayService.sendKeyEvent(events[i], imePackageName)
- }
+ // The events can be null if there isn't a way to input the character
+ // with the current key character map.
+ if (events != null) {
+ for (e in events) {
+ keyEventRelayService.sendKeyEvent(e, imePackageName)
+ }
- return
+ return
+ }
}
// Otherwise, revert to the special key event containing
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/notifications/AndroidNotificationAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/notifications/AndroidNotificationAdapter.kt
index b64dea7921..9f13818dc3 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/notifications/AndroidNotificationAdapter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/notifications/AndroidNotificationAdapter.kt
@@ -8,6 +8,7 @@ import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.android.material.color.DynamicColors
+import io.github.sds100.keymapper.MainActivity
import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.util.color
import kotlinx.coroutines.CoroutineScope
@@ -36,8 +37,8 @@ class AndroidNotificationAdapter(
setContentTitle(notification.title)
setContentText(notification.text)
- if (notification.onClickActionId != null) {
- val pendingIntent = createActionPendingIntent(notification.onClickActionId)
+ if (notification.onClickAction != null) {
+ val pendingIntent = createActionIntent(notification.onClickAction)
setContentIntent(pendingIntent)
}
@@ -58,12 +59,12 @@ class AndroidNotificationAdapter(
setVisibility(NotificationCompat.VISIBILITY_SECRET)
}
- notification.actions.forEach { action ->
+ for (action in notification.actions) {
addAction(
NotificationCompat.Action(
0,
action.text,
- createActionPendingIntent(action.id),
+ createActionIntent(action.intentType),
),
)
}
@@ -100,11 +101,29 @@ class AndroidNotificationAdapter(
}
}
- private fun createActionPendingIntent(actionId: String): PendingIntent {
- val intent = Intent(ctx, NotificationClickReceiver::class.java).apply {
- action = actionId
- }
+ private fun createActionIntent(intentType: NotificationIntentType): PendingIntent {
+ when (intentType) {
+ is NotificationIntentType.Broadcast -> {
+ val intent = Intent(ctx, NotificationClickReceiver::class.java).apply {
+ action = intentType.action
+ }
+
+ return PendingIntent.getBroadcast(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ }
- return PendingIntent.getBroadcast(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ is NotificationIntentType.MainActivity -> {
+ val intent = Intent(ctx, MainActivity::class.java).apply {
+ action = intentType.customIntentAction ?: Intent.ACTION_MAIN
+ }
+
+ return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ is NotificationIntentType.Activity -> {
+ val intent = Intent(intentType.action)
+
+ return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ }
+ }
}
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/notifications/ManageNotificationsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/system/notifications/ManageNotificationsUseCase.kt
index e3524d9227..63ffafbdab 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/notifications/ManageNotificationsUseCase.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/notifications/ManageNotificationsUseCase.kt
@@ -3,6 +3,8 @@ package io.github.sds100.keymapper.system.notifications
import android.os.Build
import io.github.sds100.keymapper.data.Keys
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
+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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -16,6 +18,7 @@ class ManageNotificationsUseCaseImpl(
private val preferences: PreferenceRepository,
private val notificationAdapter: NotificationAdapter,
private val suAdapter: SuAdapter,
+ private val permissionAdapter: PermissionAdapter,
) : ManageNotificationsUseCase {
override val showImePickerNotification: Flow =
@@ -82,6 +85,10 @@ class ManageNotificationsUseCaseImpl(
override fun deleteChannel(channelId: String) {
notificationAdapter.deleteChannel(channelId)
}
+
+ override fun isPermissionGranted(): Boolean {
+ return permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS)
+ }
}
interface ManageNotificationsUseCase {
@@ -94,6 +101,7 @@ interface ManageNotificationsUseCase {
*/
val onActionClick: Flow
+ fun isPermissionGranted(): Boolean
fun show(notification: NotificationModel)
fun dismiss(notificationId: Int)
fun createChannel(channel: NotificationChannelModel)
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationController.kt b/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationController.kt
index fbb8fcbf04..09accd310c 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationController.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationController.kt
@@ -1,13 +1,12 @@
package io.github.sds100.keymapper.system.notifications
-import androidx.annotation.VisibleForTesting
+import android.provider.Settings
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
+import io.github.sds100.keymapper.BaseMainActivity
import io.github.sds100.keymapper.Constants
-import io.github.sds100.keymapper.MainActivity
import io.github.sds100.keymapper.R
import io.github.sds100.keymapper.mappings.PauseMappingsUseCase
-import io.github.sds100.keymapper.mappings.fingerprintmaps.AreFingerprintGesturesSupportedUseCase
import io.github.sds100.keymapper.onboarding.OnboardingUseCase
import io.github.sds100.keymapper.system.accessibility.ControlAccessibilityServiceUseCase
import io.github.sds100.keymapper.system.accessibility.ServiceState
@@ -28,7 +27,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -44,7 +42,6 @@ class NotificationController(
private val controlAccessibilityService: ControlAccessibilityServiceUseCase,
private val toggleCompatibleIme: ToggleCompatibleImeUseCase,
private val hideInputMethod: ShowHideInputMethodUseCase,
- private val areFingerprintGesturesSupported: AreFingerprintGesturesSupportedUseCase,
private val onboardingUseCase: OnboardingUseCase,
private val resourceProvider: ResourceProvider,
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
@@ -55,8 +52,7 @@ class NotificationController(
private const val ID_KEYBOARD_HIDDEN = 747
private const val ID_TOGGLE_MAPPINGS = 231
private const val ID_TOGGLE_KEYBOARD = 143
- private const val ID_FEATURE_REMAP_FINGERPRINT_GESTURES = 1
- const val ID_SETUP_CHOSEN_DEVICES_AGAIN = 2
+ private const val ID_FEATURE_ASSISTANT_TRIGGER = 900
const val CHANNEL_TOGGLE_KEYMAPS = "channel_toggle_remaps"
const val CHANNEL_IME_PICKER = "channel_ime_picker"
@@ -87,29 +83,19 @@ class NotificationController(
private const val ACTION_DISMISS_TOGGLE_MAPPINGS =
"${Constants.PACKAGE_NAME}.ACTION_DISMISS_TOGGLE_MAPPINGS"
- private const val ACTION_OPEN_KEY_MAPPER =
- "${Constants.PACKAGE_NAME}.ACTION_OPEN_KEY_MAPPER"
-
private const val ACTION_SHOW_IME_PICKER =
"${Constants.PACKAGE_NAME}.ACTION_SHOW_IME_PICKER"
private const val ACTION_SHOW_KEYBOARD = "${Constants.PACKAGE_NAME}.ACTION_SHOW_KEYBOARD"
private const val ACTION_TOGGLE_KEYBOARD =
"${Constants.PACKAGE_NAME}.ACTION_TOGGLE_KEYBOARD"
-
- @VisibleForTesting
- const val ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN =
- "${Constants.PACKAGE_NAME}.ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN"
-
- private const val ACTION_FINGERPRINT_GESTURE_FEATURE =
- "${Constants.PACKAGE_NAME}.ACTION_FINGERPRINT_GESTURE_FEATURE"
}
/**
* Open the app and use the String as the Intent action.
*/
- private val _openApp: MutableSharedFlow = MutableSharedFlow()
- val openApp: SharedFlow = _openApp.asSharedFlow()
+ private val _openApp: MutableSharedFlow = MutableSharedFlow()
+ val openApp: SharedFlow = _openApp.asSharedFlow()
private val _showToast = MutableSharedFlow()
val showToast = _showToast.asSharedFlow()
@@ -118,6 +104,14 @@ class NotificationController(
manageNotifications.deleteChannel(CHANNEL_ID_WARNINGS)
manageNotifications.deleteChannel(CHANNEL_ID_PERSISTENT)
+ manageNotifications.createChannel(
+ NotificationChannelModel(
+ id = CHANNEL_NEW_FEATURES,
+ name = getString(R.string.notification_channel_new_features),
+ NotificationManagerCompat.IMPORTANCE_LOW,
+ ),
+ )
+
combine(
manageNotifications.showToggleMappingsNotification,
controlAccessibilityService.serviceState,
@@ -161,32 +155,18 @@ class NotificationController(
}.flowOn(dispatchers.default()).launchIn(coroutineScope)
coroutineScope.launch(dispatchers.default()) {
- combine(
- onboardingUseCase.showFingerprintFeatureNotificationIfAvailable,
- areFingerprintGesturesSupported.isSupported.map { it ?: false },
- ) { showIfAvailable, isSupported ->
- showIfAvailable && isSupported
- }.first { it } // suspend until the notification should be shown
-
- manageNotifications.createChannel(
- NotificationChannelModel(
- id = CHANNEL_NEW_FEATURES,
- name = getString(R.string.notification_channel_new_features),
- NotificationManagerCompat.IMPORTANCE_LOW,
- ),
- )
+ // suspend until the notification should be shown.
+ onboardingUseCase.showAssistantTriggerFeatureNotification.first { it }
- manageNotifications.show(fingerprintFeatureNotification())
- onboardingUseCase.showedFingerprintFeatureNotificationIfAvailable()
- }
+ manageNotifications.show(assistantTriggerFeatureNotification())
- onboardingUseCase.showSetupChosenDevicesAgainNotification.onEach { show ->
- if (show) {
- manageNotifications.show(setupChosenDevicesSettingsAgainNotification())
- } else {
- manageNotifications.dismiss(ID_SETUP_CHOSEN_DEVICES_AGAIN)
+ // Only save that the notification is shown if the app has
+ // permissions to show notifications so that it is shown
+ // the next time permission is granted.
+ if (manageNotifications.isPermissionGranted()) {
+ onboardingUseCase.showedAssistantTriggerFeatureNotification()
}
- }.flowOn(dispatchers.default()).launchIn(coroutineScope)
+ }
hideInputMethod.onHiddenChange.onEach { isHidden ->
manageNotifications.createChannel(
@@ -213,7 +193,6 @@ class NotificationController(
ACTION_STOP_SERVICE -> controlAccessibilityService.stopService()
ACTION_DISMISS_TOGGLE_MAPPINGS -> manageNotifications.dismiss(ID_TOGGLE_MAPPINGS)
- ACTION_OPEN_KEY_MAPPER -> _openApp.emit(null)
ACTION_SHOW_IME_PICKER -> showImePicker.show(fromForeground = false)
ACTION_SHOW_KEYBOARD -> hideInputMethod.show()
ACTION_TOGGLE_KEYBOARD -> toggleCompatibleIme.toggle().onSuccess {
@@ -221,16 +200,6 @@ class NotificationController(
}.onFailure {
_showToast.emit(it.getFullMessage(this))
}
-
- ACTION_FINGERPRINT_GESTURE_FEATURE -> {
- onboardingUseCase.approvedFingerprintFeaturePrompt = true
- _openApp.emit(null)
- }
-
- ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN -> {
- onboardingUseCase.approvedSetupChosenDevicesAgainNotification()
- _openApp.emit(null)
- }
}
}.flowOn(dispatchers.default()).launchIn(coroutineScope)
}
@@ -250,7 +219,7 @@ class NotificationController(
private fun attemptStartAccessibilityService() {
if (!controlAccessibilityService.startService()) {
coroutineScope.launch {
- _openApp.emit(MainActivity.ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG)
+ _openApp.emit(BaseMainActivity.ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG)
}
}
}
@@ -258,7 +227,7 @@ class NotificationController(
private fun attemptRestartAccessibilityService() {
if (!controlAccessibilityService.restartService()) {
coroutineScope.launch {
- _openApp.emit(MainActivity.ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG)
+ _openApp.emit(BaseMainActivity.ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG)
}
}
}
@@ -298,94 +267,135 @@ class NotificationController(
}
}
- private fun mappingsPausedNotification(): NotificationModel = NotificationModel(
- id = ID_TOGGLE_MAPPINGS,
- channel = CHANNEL_TOGGLE_KEYMAPS,
- title = getString(R.string.notification_keymaps_paused_title),
- text = getString(R.string.notification_keymaps_paused_text),
- icon = R.drawable.ic_notification_play,
- onClickActionId = ACTION_OPEN_KEY_MAPPER,
- showOnLockscreen = true,
- onGoing = true,
- priority = NotificationCompat.PRIORITY_MIN,
- actions = listOf(
- NotificationModel.Action(
- ACTION_RESUME_MAPPINGS,
- getString(R.string.notification_action_resume),
- ),
- NotificationModel.Action(
- ACTION_DISMISS_TOGGLE_MAPPINGS,
- getString(R.string.notification_action_dismiss),
- ),
- NotificationModel.Action(
- ACTION_STOP_SERVICE,
- getString(R.string.notification_action_stop_acc_service),
- ),
- ),
- )
+ private fun mappingsPausedNotification(): NotificationModel {
+ val stopServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) {
+ NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+ } else {
+ NotificationIntentType.Broadcast(ACTION_STOP_SERVICE)
+ }
- private fun mappingsResumedNotification(): NotificationModel = NotificationModel(
- id = ID_TOGGLE_MAPPINGS,
- channel = CHANNEL_TOGGLE_KEYMAPS,
- title = getString(R.string.notification_keymaps_resumed_title),
- text = getString(R.string.notification_keymaps_resumed_text),
- icon = R.drawable.ic_notification_pause,
- onClickActionId = ACTION_OPEN_KEY_MAPPER,
- showOnLockscreen = true,
- onGoing = true,
- priority = NotificationCompat.PRIORITY_MIN,
- actions = listOf(
- NotificationModel.Action(
- ACTION_PAUSE_MAPPINGS,
- getString(R.string.notification_action_pause),
- ),
- NotificationModel.Action(
- ACTION_DISMISS_TOGGLE_MAPPINGS,
- getString(R.string.notification_action_dismiss),
+ return NotificationModel(
+ id = ID_TOGGLE_MAPPINGS,
+ channel = CHANNEL_TOGGLE_KEYMAPS,
+ title = getString(R.string.notification_keymaps_paused_title),
+ text = getString(R.string.notification_keymaps_paused_text),
+ icon = R.drawable.ic_notification_play,
+ onClickAction = NotificationIntentType.MainActivity(),
+ showOnLockscreen = true,
+ onGoing = true,
+ priority = NotificationCompat.PRIORITY_MIN,
+ actions = listOf(
+ NotificationModel.Action(
+ getString(R.string.notification_action_resume),
+ NotificationIntentType.Broadcast(ACTION_RESUME_MAPPINGS),
+ ),
+ NotificationModel.Action(
+ getString(R.string.notification_action_dismiss),
+ NotificationIntentType.Broadcast(ACTION_DISMISS_TOGGLE_MAPPINGS),
+ ),
+ NotificationModel.Action(
+ getString(R.string.notification_action_stop_acc_service),
+ stopServiceAction,
+ ),
),
- NotificationModel.Action(
- ACTION_STOP_SERVICE,
- getString(R.string.notification_action_stop_acc_service),
+ )
+ }
+
+ private fun mappingsResumedNotification(): NotificationModel {
+ // Since Notification trampolines are no longer allowed, the notification
+ // must directly launch the accessibility settings instead of relaying the request
+ // through a broadcast receiver that eventually calls the ServiceAdapter.
+ val stopServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) {
+ NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+ } else {
+ NotificationIntentType.Broadcast(ACTION_STOP_SERVICE)
+ }
+
+ return NotificationModel(
+ id = ID_TOGGLE_MAPPINGS,
+ channel = CHANNEL_TOGGLE_KEYMAPS,
+ title = getString(R.string.notification_keymaps_resumed_title),
+ text = getString(R.string.notification_keymaps_resumed_text),
+ icon = R.drawable.ic_notification_pause,
+ onClickAction = NotificationIntentType.MainActivity(),
+ showOnLockscreen = true,
+ onGoing = true,
+ priority = NotificationCompat.PRIORITY_MIN,
+ actions = listOf(
+ NotificationModel.Action(
+ getString(R.string.notification_action_pause),
+ NotificationIntentType.Broadcast(ACTION_PAUSE_MAPPINGS),
+ ),
+ NotificationModel.Action(
+ getString(R.string.notification_action_dismiss),
+ NotificationIntentType.Broadcast(ACTION_DISMISS_TOGGLE_MAPPINGS),
+ ),
+ NotificationModel.Action(
+ getString(R.string.notification_action_stop_acc_service),
+ stopServiceAction,
+ ),
),
- ),
- )
+ )
+ }
- private fun accessibilityServiceDisabledNotification(): NotificationModel = NotificationModel(
- id = ID_TOGGLE_MAPPINGS,
- channel = CHANNEL_TOGGLE_KEYMAPS,
- title = getString(R.string.notification_accessibility_service_disabled_title),
- text = getString(R.string.notification_accessibility_service_disabled_text),
- icon = R.drawable.ic_notification_pause,
- onClickActionId = ACTION_START_SERVICE,
- showOnLockscreen = true,
- onGoing = true,
- priority = NotificationCompat.PRIORITY_MIN,
- actions = listOf(
- NotificationModel.Action(
- ACTION_DISMISS_TOGGLE_MAPPINGS,
- getString(R.string.notification_action_dismiss),
+ private fun accessibilityServiceDisabledNotification(): NotificationModel {
+ // Since Notification trampolines are no longer allowed, the notification
+ // must directly launch the accessibility settings instead of relaying the request
+ // through a broadcast receiver that eventually calls the ServiceAdapter.
+ val onClickAction = if (controlAccessibilityService.isUserInteractionRequired()) {
+ NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+ } else {
+ NotificationIntentType.Broadcast(ACTION_START_SERVICE)
+ }
+
+ return NotificationModel(
+ id = ID_TOGGLE_MAPPINGS,
+ channel = CHANNEL_TOGGLE_KEYMAPS,
+ title = getString(R.string.notification_accessibility_service_disabled_title),
+ text = getString(R.string.notification_accessibility_service_disabled_text),
+ icon = R.drawable.ic_notification_pause,
+ onClickAction = onClickAction,
+ showOnLockscreen = true,
+ onGoing = true,
+ priority = NotificationCompat.PRIORITY_MIN,
+ actions = listOf(
+ NotificationModel.Action(
+ getString(R.string.notification_action_dismiss),
+ NotificationIntentType.Broadcast(ACTION_DISMISS_TOGGLE_MAPPINGS),
+ ),
),
- ),
- )
+ )
+ }
- private fun accessibilityServiceCrashedNotification(): NotificationModel = NotificationModel(
- id = ID_TOGGLE_MAPPINGS,
- channel = CHANNEL_TOGGLE_KEYMAPS,
- title = getString(R.string.notification_accessibility_service_crashed_title),
- text = getString(R.string.notification_accessibility_service_crashed_text),
- icon = R.drawable.ic_notification_pause,
- onClickActionId = ACTION_RESTART_SERVICE,
- showOnLockscreen = true,
- onGoing = true,
- priority = NotificationCompat.PRIORITY_MIN,
- bigTextStyle = true,
- actions = listOf(
- NotificationModel.Action(
- ACTION_RESTART_SERVICE,
- getString(R.string.notification_action_restart_accessibility_service),
+ private fun accessibilityServiceCrashedNotification(): NotificationModel {
+ // Since Notification trampolines are no longer allowed, the notification
+ // must directly launch the accessibility settings instead of relaying the request
+ // through a broadcast receiver that eventually calls the ServiceAdapter.
+ val onClickAction = if (controlAccessibilityService.isUserInteractionRequired()) {
+ NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+ } else {
+ NotificationIntentType.Broadcast(ACTION_RESTART_SERVICE)
+ }
+
+ return NotificationModel(
+ id = ID_TOGGLE_MAPPINGS,
+ channel = CHANNEL_TOGGLE_KEYMAPS,
+ title = getString(R.string.notification_accessibility_service_crashed_title),
+ text = getString(R.string.notification_accessibility_service_crashed_text),
+ icon = R.drawable.ic_notification_pause,
+ onClickAction = onClickAction,
+ showOnLockscreen = true,
+ onGoing = true,
+ priority = NotificationCompat.PRIORITY_MIN,
+ bigTextStyle = true,
+ actions = listOf(
+ NotificationModel.Action(
+ getString(R.string.notification_action_restart_accessibility_service),
+ onClickAction,
+ ),
),
- ),
- )
+ )
+ }
private fun imePickerNotification(): NotificationModel = NotificationModel(
id = ID_IME_PICKER,
@@ -393,7 +403,7 @@ class NotificationController(
title = getString(R.string.notification_ime_persistent_title),
text = getString(R.string.notification_ime_persistent_text),
icon = R.drawable.ic_notification_keyboard,
- onClickActionId = ACTION_SHOW_IME_PICKER,
+ onClickAction = NotificationIntentType.Broadcast(ACTION_SHOW_IME_PICKER),
showOnLockscreen = false,
onGoing = true,
priority = NotificationCompat.PRIORITY_MIN,
@@ -405,56 +415,40 @@ class NotificationController(
title = getString(R.string.notification_toggle_keyboard_title),
text = getString(R.string.notification_toggle_keyboard_text),
icon = R.drawable.ic_notification_keyboard,
- onClickActionId = null,
showOnLockscreen = true,
onGoing = true,
priority = NotificationCompat.PRIORITY_MIN,
actions = listOf(
NotificationModel.Action(
- ACTION_TOGGLE_KEYBOARD,
getString(R.string.notification_toggle_keyboard_action),
+ intentType = NotificationIntentType.Broadcast(ACTION_TOGGLE_KEYBOARD),
),
),
)
- private fun fingerprintFeatureNotification(): NotificationModel = NotificationModel(
- id = ID_FEATURE_REMAP_FINGERPRINT_GESTURES,
- channel = CHANNEL_NEW_FEATURES,
- title = getString(R.string.notification_feature_fingerprint_title),
- text = getString(R.string.notification_feature_fingerprint_text),
- icon = R.drawable.ic_notification_fingerprint,
- onClickActionId = ACTION_FINGERPRINT_GESTURE_FEATURE,
- priority = NotificationCompat.PRIORITY_LOW,
- autoCancel = true,
- onGoing = false,
- showOnLockscreen = false,
- bigTextStyle = true,
- )
-
- private fun setupChosenDevicesSettingsAgainNotification(): NotificationModel =
- NotificationModel(
- id = ID_SETUP_CHOSEN_DEVICES_AGAIN,
- channel = CHANNEL_NEW_FEATURES,
- title = getString(R.string.notification_setup_chosen_devices_again_title),
- text = getString(R.string.notification_setup_chosen_devices_again_text),
- icon = R.drawable.ic_notification_settings,
- onClickActionId = ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN,
- priority = NotificationCompat.PRIORITY_LOW,
- autoCancel = true,
- onGoing = false,
- showOnLockscreen = false,
- bigTextStyle = true,
- )
-
private fun keyboardHiddenNotification(): NotificationModel = NotificationModel(
id = ID_KEYBOARD_HIDDEN,
channel = CHANNEL_KEYBOARD_HIDDEN,
title = getString(R.string.notification_keyboard_hidden_title),
text = getString(R.string.notification_keyboard_hidden_text),
icon = R.drawable.ic_notification_keyboard_hide,
- onClickActionId = ACTION_SHOW_KEYBOARD,
+ onClickAction = NotificationIntentType.Broadcast(ACTION_SHOW_KEYBOARD),
showOnLockscreen = false,
onGoing = true,
priority = NotificationCompat.PRIORITY_LOW,
)
+
+ private fun assistantTriggerFeatureNotification(): NotificationModel = NotificationModel(
+ id = ID_FEATURE_ASSISTANT_TRIGGER,
+ channel = CHANNEL_NEW_FEATURES,
+ title = getString(R.string.notification_assistant_trigger_feature_title),
+ text = getString(R.string.notification_assistant_trigger_feature_text),
+ icon = R.drawable.ic_outline_assistant_24,
+ onClickAction = NotificationIntentType.MainActivity(BaseMainActivity.ACTION_USE_ASSISTANT_TRIGGER),
+ priority = NotificationCompat.PRIORITY_LOW,
+ autoCancel = true,
+ onGoing = false,
+ showOnLockscreen = false,
+ bigTextStyle = true,
+ )
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt b/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt
index 2a7c627aea..f7e72a1406 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt
@@ -12,10 +12,9 @@ data class NotificationModel(
val text: String,
@DrawableRes val icon: Int,
/**
- * The id to send back to the notification id when the notification is tapped or null if nothing
- * should happen when the notification is tapped.
+ * Null if nothing should happen when the notification is tapped.
*/
- val onClickActionId: String?,
+ val onClickAction: NotificationIntentType? = null,
val showOnLockscreen: Boolean,
val onGoing: Boolean,
val priority: Int,
@@ -23,6 +22,24 @@ data class NotificationModel(
val autoCancel: Boolean = false,
val bigTextStyle: Boolean = false,
) {
+ data class Action(val text: String, val intentType: NotificationIntentType)
+}
+
+/**
+ * Due to restrictions on notification trampolines in Android 12+ you can't launch
+ * activities from a broadcast receiver in response to a notification action.
+ */
+sealed class NotificationIntentType {
+ /**
+ * Broadcast an intent to the NotificationReceiver.
+ */
+ data class Broadcast(val action: String) : NotificationIntentType()
+
+ /**
+ * Launch the main activity with the specified action in the intent. If it is null
+ * then it will just launch the activity without a custom action.
+ */
+ data class MainActivity(val customIntentAction: String? = null) : NotificationIntentType()
- data class Action(val id: String, val text: String)
+ data class Activity(val action: String) : NotificationIntentType()
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt
index 7a59597533..386a8fd003 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt
@@ -21,13 +21,17 @@ import io.github.sds100.keymapper.data.repositories.PreferenceRepository
import io.github.sds100.keymapper.shizuku.ShizukuUtils
import io.github.sds100.keymapper.system.DeviceAdmin
import io.github.sds100.keymapper.system.accessibility.ServiceAdapter
+import io.github.sds100.keymapper.system.apps.PackageManagerAdapter
import io.github.sds100.keymapper.system.root.SuAdapter
import io.github.sds100.keymapper.util.Error
import io.github.sds100.keymapper.util.Result
+import io.github.sds100.keymapper.util.Success
import io.github.sds100.keymapper.util.getIdentifier
import io.github.sds100.keymapper.util.onFailure
import io.github.sds100.keymapper.util.onSuccess
import io.github.sds100.keymapper.util.success
+import io.github.sds100.keymapper.util.then
+import io.github.sds100.keymapper.util.valueIfFailure
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -56,6 +60,7 @@ class AndroidPermissionAdapter(
private val suAdapter: SuAdapter,
private val notificationReceiverAdapter: ServiceAdapter,
private val preferenceRepository: PreferenceRepository,
+ private val packageManagerAdapter: PackageManagerAdapter,
) : PermissionAdapter {
companion object {
const val REQUEST_CODE_SHIZUKU_PERMISSION = 1
@@ -333,6 +338,11 @@ class AndroidPermissionAdapter(
} else {
true
}
+
+ Permission.DEVICE_ASSISTANT ->
+ packageManagerAdapter.getDeviceAssistantPackage()
+ .then { Success(it == Constants.PACKAGE_NAME) }
+ .valueIfFailure { false }
}
override fun isGrantedFlow(permission: Permission): Flow = callbackFlow {
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt b/app/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt
index 43319c74eb..2bd83e87a0 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/permissions/Permission.kt
@@ -19,4 +19,5 @@ enum class Permission {
ANSWER_PHONE_CALL,
FIND_NEARBY_DEVICES,
POST_NOTIFICATIONS,
+ DEVICE_ASSISTANT,
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/system/permissions/RequestPermissionDelegate.kt b/app/src/main/java/io/github/sds100/keymapper/system/permissions/RequestPermissionDelegate.kt
index 2089876320..1004393c73 100644
--- a/app/src/main/java/io/github/sds100/keymapper/system/permissions/RequestPermissionDelegate.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/system/permissions/RequestPermissionDelegate.kt
@@ -100,6 +100,16 @@ class RequestPermissionDelegate(
Permission.POST_NOTIFICATIONS -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
+
+ Permission.DEVICE_ASSISTANT -> {
+ try {
+ Intent(Settings.ACTION_VOICE_INPUT_SETTINGS).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ startActivityForResultLauncher.launch(this)
+ }
+ } catch (e: ActivityNotFoundException) {
+ }
+ }
}
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt b/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt
index eb97c58a26..96bed81db7 100644
--- a/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt
@@ -2,6 +2,7 @@ package io.github.sds100.keymapper.util
import android.content.pm.PackageManager
import io.github.sds100.keymapper.R
+import io.github.sds100.keymapper.purchasing.ProductId
import io.github.sds100.keymapper.system.BuildUtils
import io.github.sds100.keymapper.util.ui.ResourceProvider
@@ -9,7 +10,7 @@ import io.github.sds100.keymapper.util.ui.ResourceProvider
* Created by sds100 on 29/02/2020.
*/
-fun Error.getFullMessage(resourceProvider: ResourceProvider) = when (this) {
+fun Error.getFullMessage(resourceProvider: ResourceProvider): String = when (this) {
is Error.PermissionDenied ->
Error.PermissionDenied.getMessageForPermission(
resourceProvider,
@@ -141,6 +142,18 @@ fun Error.getFullMessage(resourceProvider: ResourceProvider) = when (this) {
Error.CantDetectKeyEventsInPhoneCall -> resourceProvider.getString(R.string.trigger_error_cant_detect_in_phone_call_explanation)
Error.GestureStrokeCountTooHigh -> resourceProvider.getString(R.string.trigger_error_gesture_stroke_count_too_high)
Error.GestureDurationTooHigh -> resourceProvider.getString(R.string.trigger_error_gesture_duration_too_high)
+
+ Error.PurchasingError.Cancelled -> resourceProvider.getString(R.string.purchasing_error_cancelled)
+ Error.PurchasingError.NetworkError -> resourceProvider.getString(R.string.purchasing_error_network)
+ Error.PurchasingError.ProductNotFound -> resourceProvider.getString(R.string.purchasing_error_product_not_found)
+ Error.PurchasingError.StoreProblem -> resourceProvider.getString(R.string.purchasing_error_store_problem)
+ is Error.PurchasingError.Unexpected -> this.message
+
+ is Error.ProductNotPurchased -> when (this.product) {
+ ProductId.ASSISTANT_TRIGGER -> resourceProvider.getString(R.string.purchasing_error_assistant_not_purchased_home_screen)
+ }
+
+ Error.PurchasingNotImplemented -> resourceProvider.getString(R.string.purchasing_error_not_implemented)
}
val Error.isFixable: Boolean
diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt
index cea744e65c..2914328dd9 100644
--- a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt
@@ -140,6 +140,7 @@ object Inject {
UseCases.displayKeyMap(ctx),
UseCases.createAction(ctx),
ServiceLocator.resourceProvider(ctx),
+ ServiceLocator.purchasingManager(ctx),
)
fun configFingerprintMapViewModel(
diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Result.kt b/app/src/main/java/io/github/sds100/keymapper/util/Result.kt
index 9399cba72b..222cfafa67 100644
--- a/app/src/main/java/io/github/sds100/keymapper/util/Result.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/util/Result.kt
@@ -1,6 +1,7 @@
package io.github.sds100.keymapper.util
import io.github.sds100.keymapper.R
+import io.github.sds100.keymapper.purchasing.ProductId
import io.github.sds100.keymapper.system.inputmethod.ImeInfo
import io.github.sds100.keymapper.system.permissions.Permission
import io.github.sds100.keymapper.util.ui.ResourceProvider
@@ -24,37 +25,37 @@ sealed class Error : Result() {
data class SdkVersionTooLow(val minSdk: Int) : Error()
data class SdkVersionTooHigh(val maxSdk: Int) : Error()
data class InputMethodNotFound(val imeLabel: String) : Error()
- object NoVoiceAssistant : Error()
- object NoDeviceAssistant : Error()
- object NoCameraApp : Error()
- object NoSettingsApp : Error()
- object FrontFlashNotFound : Error()
- object BackFlashNotFound : Error()
+ data object NoVoiceAssistant : Error()
+ data object NoDeviceAssistant : Error()
+ data object NoCameraApp : Error()
+ data object NoSettingsApp : Error()
+ data object FrontFlashNotFound : Error()
+ data object BackFlashNotFound : Error()
data class ImeDisabled(val ime: ImeInfo) : Error()
data class DeviceNotFound(val descriptor: String) : Error()
- object InvalidNumber : Error()
+ data object InvalidNumber : Error()
data class NumberTooBig(val max: Int) : Error()
data class NumberTooSmall(val min: Int) : Error()
- object EmptyText : Error()
- object NoIncompatibleKeyboardsInstalled : Error()
- object NoMediaSessions : Error()
- object BackupVersionTooNew : Error()
- object LauncherShortcutsNotSupported : Error()
+ data object EmptyText : Error()
+ data object NoIncompatibleKeyboardsInstalled : Error()
+ data object NoMediaSessions : Error()
+ data object BackupVersionTooNew : Error()
+ data object LauncherShortcutsNotSupported : Error()
data class AppNotFound(val packageName: String) : Error()
data class AppDisabled(val packageName: String) : Error()
- object AppShortcutCantBeOpened : Error()
- object InsufficientPermissionsToOpenAppShortcut : Error()
- object NoCompatibleImeEnabled : Error()
- object NoCompatibleImeChosen : Error()
+ data object AppShortcutCantBeOpened : Error()
+ data object InsufficientPermissionsToOpenAppShortcut : Error()
+ data object NoCompatibleImeEnabled : Error()
+ data object NoCompatibleImeChosen : Error()
- object AccessibilityServiceDisabled : Error()
- object AccessibilityServiceCrashed : Error()
+ data object AccessibilityServiceDisabled : Error()
+ data object AccessibilityServiceCrashed : Error()
- object CantShowImePickerInBackground : Error()
- object CantFindImeSettings : Error()
- object GestureStrokeCountTooHigh : Error()
- object GestureDurationTooHigh : Error()
+ data object CantShowImePickerInBackground : Error()
+ data object CantFindImeSettings : Error()
+ data object GestureStrokeCountTooHigh : Error()
+ data object GestureDurationTooHigh : Error()
data class PermissionDenied(val permission: Permission) : Error() {
companion object {
@@ -79,6 +80,7 @@ sealed class Error : Result() {
Permission.ANSWER_PHONE_CALL -> R.string.error_answer_end_phone_call_permission_denied
Permission.FIND_NEARBY_DEVICES -> R.string.error_find_nearby_devices_permission_denied
Permission.POST_NOTIFICATIONS -> R.string.error_notifications_permission_denied
+ Permission.DEVICE_ASSISTANT -> R.string.trigger_error_assistant_activity_not_chosen_short
}
return resourceProvider.getString(resId)
@@ -86,40 +88,54 @@ sealed class Error : Result() {
}
}
- object FailedToFindAccessibilityNode : Error()
+ data object FailedToFindAccessibilityNode : Error()
data class FailedToPerformAccessibilityGlobalAction(val action: Int) : Error()
- object FailedToDispatchGesture : Error()
+ data object FailedToDispatchGesture : Error()
- object CameraInUse : Error()
- object CameraDisconnected : Error()
- object CameraDisabled : Error()
- object MaxCamerasInUse : Error()
- object CameraError : Error()
+ data object CameraInUse : Error()
+ data object CameraDisconnected : Error()
+ data object CameraDisabled : Error()
+ data object MaxCamerasInUse : Error()
+ data object CameraError : Error()
data class FailedToModifySystemSetting(val setting: String) : Error()
- object FailedToChangeIme : Error()
- object NoAppToOpenUrl : Error()
- object NoAppToPhoneCall : Error()
+ data object FailedToChangeIme : Error()
+ data object NoAppToOpenUrl : Error()
+ data object NoAppToPhoneCall : Error()
data class NotAFile(val uri: String) : Error()
data class NotADirectory(val uri: String) : Error()
- object StoragePermissionDenied : Error()
+ data object StoragePermissionDenied : Error()
data class CannotCreateFileInTarget(val uri: String) : Error()
data class SourceFileNotFound(val uri: String) : Error()
data class TargetFileNotFound(val uri: String) : Error()
data class TargetDirectoryNotFound(val uri: String) : Error()
- object UnknownIOError : Error()
- object FileOperationCancelled : Error()
- object TargetDirectoryMatchesSourceDirectory : Error()
+ data object UnknownIOError : Error()
+ data object FileOperationCancelled : Error()
+ data object TargetDirectoryMatchesSourceDirectory : Error()
data class NoSpaceLeftOnTarget(val uri: String) : Error()
- object NoFileName : Error()
+ data object NoFileName : Error()
- object EmptyJson : Error()
- object CantFindSoundFile : Error()
+ data object EmptyJson : Error()
+ data object CantFindSoundFile : Error()
data class CorruptJsonFile(val reason: String) : Error()
- object ShizukuNotStarted : Error()
- object CantDetectKeyEventsInPhoneCall : Error()
+ data object ShizukuNotStarted : Error()
+ data object CantDetectKeyEventsInPhoneCall : Error()
+
+ // This is returned from the PurchasingManager on FOSS builds that don't
+ // have the pro features implemented.
+ data object PurchasingNotImplemented : Error()
+
+ data class ProductNotPurchased(val product: ProductId) : Error()
+
+ sealed class PurchasingError : Error() {
+ data object ProductNotFound : PurchasingError()
+ data object Cancelled : PurchasingError()
+ data object StoreProblem : PurchasingError()
+ data object NetworkError : PurchasingError()
+ data class Unexpected(val message: String) : PurchasingError()
+ }
}
inline fun Result.onSuccess(f: (T) -> Unit): Result {
@@ -156,6 +172,15 @@ inline infix fun Result.otherwise(f: (error: Error) -> Result) =
is Error -> f(this)
}
+inline fun Result.resolve(
+ onSuccess: (value: T) -> U,
+ onFailure: (error: Error) -> U,
+) =
+ when (this) {
+ is Success -> onSuccess(this.value)
+ is Error -> onFailure(this)
+ }
+
inline infix fun Result.valueIfFailure(f: (error: Error) -> T): T =
when (this) {
is Success -> this.value
diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ShareUtils.kt b/app/src/main/java/io/github/sds100/keymapper/util/ShareUtils.kt
new file mode 100644
index 0000000000..888bbe2f94
--- /dev/null
+++ b/app/src/main/java/io/github/sds100/keymapper/util/ShareUtils.kt
@@ -0,0 +1,19 @@
+package io.github.sds100.keymapper.util
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+
+object ShareUtils {
+ fun sendMail(ctx: Context, email: String, subject: String, body: String) {
+ try {
+ val intent = Intent(Intent.ACTION_SEND)
+ intent.type = "vnd.android.cursor.item/email"
+ intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject)
+ intent.putExtra(Intent.EXTRA_TEXT, body)
+ ctx.startActivity(intent)
+ } catch (_: ActivityNotFoundException) {
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/sds100/keymapper/util/VersionHelper.kt b/app/src/main/java/io/github/sds100/keymapper/util/VersionHelper.kt
index 456ef74df2..9f0fd61c77 100644
--- a/app/src/main/java/io/github/sds100/keymapper/util/VersionHelper.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/util/VersionHelper.kt
@@ -11,4 +11,9 @@ object VersionHelper {
* availability of fingerprint gestures.
*/
const val FINGERPRINT_GESTURES_MIN_VERSION = 40
+
+ /**
+ * This is version 2.7.0 when the assistant trigger was first introduced.
+ */
+ const val ASSISTANT_TRIGGER_MIN_VERSION = 66
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavDestination.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavDestination.kt
index d18cd344e8..d6efa8c902 100644
--- a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavDestination.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavDestination.kt
@@ -94,6 +94,6 @@ sealed class NavDestination {
object FixAppKilling : NavDestination()
object Settings : NavDestination()
object About : NavDestination()
- data class ConfigKeyMap(val keyMapUid: String?) : NavDestination()
+ data class ConfigKeyMap(val keyMapUid: String?, val showAdvancedTriggers: Boolean = false) : NavDestination()
data class ConfigFingerprintMap(val id: FingerprintMapId) : NavDestination()
}
diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt
index 23167aab10..ac26bc8fa4 100644
--- a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt
+++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt
@@ -43,7 +43,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -215,7 +214,10 @@ fun NavigationViewModel.setupNavigation(fragment: Fragment) {
NavAppDirections.actionToConfigFingerprintMap(destination.id.toString())
is NavDestination.ConfigKeyMap ->
- NavAppDirections.actionToConfigKeymap(destination.keyMapUid)
+ NavAppDirections.actionToConfigKeymap(
+ destination.keyMapUid,
+ showAdvancedTriggers = destination.showAdvancedTriggers,
+ )
}
fragment.findNavController().navigate(direction)
diff --git a/app/src/main/res/layout-w1000dp-h400dp-land/fragment_trigger.xml b/app/src/main/res/layout-w1000dp-h400dp-land/fragment_trigger.xml
index bcb30aa085..e448caf5d5 100644
--- a/app/src/main/res/layout-w1000dp-h400dp-land/fragment_trigger.xml
+++ b/app/src/main/res/layout-w1000dp-h400dp-land/fragment_trigger.xml
@@ -9,7 +9,7 @@
+ type="io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel" />
@@ -36,6 +36,7 @@
android:id="@+id/listLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
+ android:layout_marginTop="8dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toTopOf="@id/radioGroupClickType"
app:layout_constraintEnd_toEndOf="parent"
@@ -46,6 +47,7 @@
android:id="@+id/recyclerViewTriggerKeys"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/list_item_simple" />
@@ -134,7 +136,7 @@
android:checkedButton="@{viewModel.checkedTriggerModeRadioButton}"
android:gravity="bottom"
android:orientation="horizontal"
- app:layout_constraintBottom_toTopOf="@+id/buttonRecordKeys"
+ app:layout_constraintBottom_toTopOf="@+id/composeViewRecordTriggerButtons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
@@ -166,21 +168,19 @@
-
diff --git a/app/src/main/res/layout-w600dp-land/fragment_trigger.xml b/app/src/main/res/layout-w600dp-land/fragment_trigger.xml
index 0d69de3ffc..1568310d6a 100644
--- a/app/src/main/res/layout-w600dp-land/fragment_trigger.xml
+++ b/app/src/main/res/layout-w600dp-land/fragment_trigger.xml
@@ -10,7 +10,7 @@
+ type="io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel" />
@@ -137,7 +138,7 @@
android:checkedButton="@{viewModel.checkedTriggerModeRadioButton}"
android:gravity="bottom"
android:orientation="horizontal"
- app:layout_constraintBottom_toTopOf="@+id/buttonRecordKeys"
+ app:layout_constraintBottom_toTopOf="@+id/composeViewRecordTriggerButtons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/recyclerViewError">
@@ -145,18 +146,18 @@
android:id="@+id/radioButtonParallel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:onCheckedChanged="@{(view, isChecked) -> viewModel.onParallelRadioButtonCheckedChange(isChecked)}"
android:layout_weight="0.5"
android:enabled="@{viewModel.triggerModeButtonsEnabled}"
+ android:onCheckedChanged="@{(view, isChecked) -> viewModel.onParallelRadioButtonCheckedChange(isChecked)}"
android:text="@string/radio_button_parallel" />
-
diff --git a/app/src/main/res/layout-w900dp-h600dp/fragment_trigger.xml b/app/src/main/res/layout-w900dp-h600dp/fragment_trigger.xml
index 8531fb1849..425ac271bf 100644
--- a/app/src/main/res/layout-w900dp-h600dp/fragment_trigger.xml
+++ b/app/src/main/res/layout-w900dp-h600dp/fragment_trigger.xml
@@ -9,7 +9,7 @@
+ type="io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel" />
@@ -131,7 +132,7 @@
android:checkedButton="@{viewModel.checkedTriggerModeRadioButton}"
android:gravity="bottom"
android:orientation="horizontal"
- app:layout_constraintBottom_toTopOf="@+id/buttonRecordKeys"
+ app:layout_constraintBottom_toTopOf="@+id/composeViewRecordTriggerButtons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/radioGroupClickType">
@@ -139,18 +140,18 @@
android:id="@+id/radioButtonParallel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:onCheckedChanged="@{(view, isChecked) -> viewModel.onParallelRadioButtonCheckedChange(isChecked)}"
android:layout_weight="0.5"
android:enabled="@{viewModel.triggerModeButtonsEnabled}"
+ android:onCheckedChanged="@{(view, isChecked) -> viewModel.onParallelRadioButtonCheckedChange(isChecked)}"
android:text="@string/radio_button_parallel" />
@@ -163,21 +164,19 @@
android:visibility="gone" />
-
diff --git a/app/src/main/res/layout/fragment_compose_view.xml b/app/src/main/res/layout/fragment_compose_view.xml
new file mode 100644
index 0000000000..cf6151272e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_compose_view.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_trigger.xml b/app/src/main/res/layout/fragment_trigger.xml
index bcb30aa085..9b12c7e24d 100644
--- a/app/src/main/res/layout/fragment_trigger.xml
+++ b/app/src/main/res/layout/fragment_trigger.xml
@@ -9,7 +9,7 @@
+ type="io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel" />
@@ -36,6 +36,7 @@
android:id="@+id/listLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
+ android:layout_marginTop="8dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toTopOf="@id/radioGroupClickType"
app:layout_constraintEnd_toEndOf="parent"
@@ -67,10 +68,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:layout_marginStart="16dp"
- android:layout_marginTop="16dp"
- android:layout_marginEnd="16dp"
- android:layout_marginBottom="16dp"
+ android:layout_marginStart="32dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginEnd="32dp"
+ android:layout_marginBottom="32dp"
android:text="@string/triggers_recyclerview_placeholder" />
@@ -101,7 +102,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
- android:text="@string/radio_button_long_press" />
+ android:text="@string/radio_button_long_press"
+ android:visibility="@{viewModel.longPressButtonVisible ? View.VISIBLE : View.GONE}" />
@@ -134,7 +137,8 @@
android:checkedButton="@{viewModel.checkedTriggerModeRadioButton}"
android:gravity="bottom"
android:orientation="horizontal"
- app:layout_constraintBottom_toTopOf="@+id/buttonRecordKeys"
+ android:visibility="@{viewModel.triggerModeRadioButtonsVisible ? View.VISIBLE : View.GONE}"
+ app:layout_constraintBottom_toTopOf="@+id/composeViewRecordTriggerButtons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
@@ -166,21 +170,19 @@
-
diff --git a/app/src/main/res/layout/list_item_trigger_key.xml b/app/src/main/res/layout/list_item_trigger_key.xml
index 940286e96b..1e203fba97 100644
--- a/app/src/main/res/layout/list_item_trigger_key.xml
+++ b/app/src/main/res/layout/list_item_trigger_key.xml
@@ -6,6 +6,7 @@
+
+ app:layout_constraintTop_toTopOf="@+id/buttonRemove">
+ app:srcCompat="@drawable/ic_baseline_devices_other_24" />
+ app:srcCompat="@drawable/ic_outline_more_vert_24" />
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_app.xml b/app/src/main/res/navigation/nav_app.xml
index 5fa5173b87..c1d17bd810 100644
--- a/app/src/main/res/navigation/nav_app.xml
+++ b/app/src/main/res/navigation/nav_app.xml
@@ -36,6 +36,10 @@
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
+
+
+
+ android:label="TriggerKeyOptionsFragment" />
+ android:label="KeymapActionOptionsFragment" />
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index d6e7f7cd6a..e9d1219a98 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -64,4 +64,6 @@
+
+
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 586b487fd6..92be005509 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -7,7 +7,6 @@
PovolitOtevřít¯\\_(ツ)_/¯\n\nTady nic není!
- Nahrajte spoušť!Přidat akci!¯\\_(ツ)_/¯\n\nVytvořte klíčovou mapu!¯\\_(ツ)_/¯\n\nNevybrali jste žádné akce pro tohoto zástupce!
@@ -159,10 +158,6 @@
AkceSpouštěčOtisk prstu
-
- @string/tab_keyevents
- \@řetězec/tab_otisk
-
@@ -1015,4 +1010,6 @@ Pokud by měla být spárována nějaká WiFi síť, nechte ji prázdnou.Překladatel (česky)Překladač (španělština)
+
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 8b2b1737b0..aef0a460ed 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -63,4 +63,6 @@
+
+
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 90b3e8b743..370ef81eb7 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -7,7 +7,6 @@
ActivarAbrir¯\\_(ツ)_/¯\n\nNada por aquí!
- ¡Graba un disparador!¡Añadir una acción!¯\\_(ツ)_/¯\n\n¡Crea un mapeado!¯\\_(ツ)_/¯\n\n¡No has escogido acciones para este atajo!
@@ -161,10 +160,6 @@
AccionesActivadorHuella digital
-
- @string/tab_keyevents
- @string/tab_fingerprint
-
@@ -1034,4 +1029,6 @@
Traductor (Checo)Traductor (Español)
+
+
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 8b2b1737b0..aef0a460ed 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -63,4 +63,6 @@
+
+
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 87efd24782..39afe2afc1 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -64,4 +64,6 @@
+
+
diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml
index 8b2b1737b0..aef0a460ed 100644
--- a/app/src/main/res/values-ka/strings.xml
+++ b/app/src/main/res/values-ka/strings.xml
@@ -63,4 +63,6 @@
+
+
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 8b2b1737b0..aef0a460ed 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -63,4 +63,6 @@
+
+
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 3efa3bf9c6..12cf32e91b 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -7,7 +7,6 @@
WłączOtwórz¯\\_(ツ)_/¯\n\nNic tu nie ma!
- Wprowadź wyzwalacz!Dodaj działanie!¯\\_(ツ)_/¯\n\nUtwórz mapę klawiszy!¯\\_(ツ)_/¯\n\nNie wybrano żadnych działań dla tego skrótu!
@@ -176,10 +175,6 @@
DziałaniaWyzwalaczOdcisk palca
-
- @string/tab_keyevents
- @string/tab_fingerprint
-
@@ -1074,4 +1069,6 @@
Tłumacz (czeski)Tłumacz (hiszpański)
+
+
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 8b2b1737b0..c7dc6c31cd 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -1,66 +1,1073 @@
+ Liberte suas chaves!
+ Sem ação definida
+ Este aplicativo requer o serviço de acessibilidade. Isso detecta suas ações fora do aplicativo. O mapeamento depende disso. Também obrigatório para criar gatilhos.
+ %d selecionado
+ Habilitar
+ Abrir
+ ¯\\_(ツ)_/¯\n\nNada encontrado!
+ Adicionar ação!
+ ¯\\_(ツ)_/¯\n\nCriar mapeamento!
+ ¯\\_(ツ)_/¯\n\nNenhuma ação para este atalho!
+ Nenhum mapeamento criado!
+ Tudo em ordem!
+ Aplicativo funcional, pode ser necessário ajuste adicional dependendo do uso.
+ O seu dispositivo não é compatível com algumas ações.
+ Ações não suportadas
+ Requer root
+ Pressione…
+ Sem ação
+ Sem gatilho
+ Código desconhecido: %s
+ Dispositivo desconhecido
+ Ativo
+ Desligado
+ Padrão do sistema
+ Este dispositivo
+ Qualquer dispositivo
+ Nome deste dispositivo desconhecido
+ Padrão
+ Ativar serviço de acessibilidade
+ Reiniciar o serviço de acessibilidade
+ Compartilhar
+ Parar repetição…
+ Gatilho liberado
+ Gatilho repetido
+ Limite atingido
+ Desliza novamente
+ Gatilho liberado
+ Gatilho repetido
+ Mostrar apps ocultos
+ Modificador
+ Alternar
+ IMPORTANTE!!! Essas coordenadas estão corretas apenas quando sua tela está na mesma orientação que a captura de tela! Esta ação cancelará qualquer toque ou gesto que você esteja fazendo na tela.\n\nSe precisar de ajuda para encontrar as coordenadas de um ponto na sua tela, tire uma captura de tela e toque na captura onde você deseja que esta ação pressione.
+ Nota: Ao usar \"pinçar para dentro\", X e Y são as coordenadas FINAIS; ao usar \"pinçar para fora\", X e Y são as coordenadas INICIAIS.
+ Dispositivo desconhecido!
+ Ações para correção!
+ Eventos para correção!
+ Realizar ações
+ Pressionado até o gatilho…
+ Sem dispositivo
+ ¯\\_(ツ)_/¯\n\nSem extras!
+ Criar novo mapa
+ Configuração da ação do evento de tecla concluída
+ Coordenada selecionada
+ Key Mapper registros
+ Enviar para
+ Novidades
+ Clique na tecla do dispositivo que você deseja que seja inserida.
+ \n\nIMPORTANTE!
+ Inserir esta tecla como uma ação funcionará apenas se você estiver usando um teclado compatível com o aplicativo.
+ IMPORTANTE!
+ Inserir este código de tecla como uma ação funcionará apenas se você estiver usando um teclado compatível com o aplicativo.
+ O arquivo de som será copiado para a pasta de dados privados do aplicativo, o que significa que suas ações ainda funcionarão mesmo se o arquivo for movido ou excluído. Ele também será salvo com seus mapas de teclas na pasta compactada.
+ Você pode excluir arquivos de som salvos nas configurações.
+ Digite algum texto que você deseja que seja inserido ao realizar esta ação.
+ Digite o número de telefone.
+ Digite um URL que você deseja abrir. O http://, https:// ou www. Não são necessários.
+ Não foi possível encontrar dispositivos pareados. O bluetooth está ligado?
+ A opção \"Permitir que outros aplicativos acionem este mapa de teclas\" será ativada para o mapa de teclas que você selecionar, se ainda não estiver ativada. Se você desativar essa opção mais tarde, quaisquer atalhos ou intenções para acionar este mapa de teclas não funcionarão.
+ Ativado
+ Desativado
+ Redefinir
+ Visto que você tenha ativado o administrador do dispositivo, deve DESATIVÁ-LO se quiser desinstalar o aplicativo.
+ Adicione uma restrição!
+ Aguarde %sms
+ Iniciar atividade: %s
+ Iniciar serviço: %s
+ Enviar transmissão: %s
+ UUID do mapa de teclas
+ Usar shell (ROOT necessário)
+ Key Mapper precisa de permissão para modificar o modo Não Perturbar se você quer que os botões funcionem corretamente no modo Não Perturbar!
+ Este acionador não funcionará como esperado no modo Não Perturbe!
+ A opção para acionar quando a tela está desligada precisa de permissão de super usuário para funcionar!
+ A opção para acionar quando a tela está desligada não funcionará!
+ Este acionador não funcionará enquanto o telefone estiver tocando ou durante uma chamada!
+ O sistema operacional não permite que os serviços de acessibilidade detectem os pressionamentos do botão de volume enquanto seu telefone está tocando ou durante uma chamada, mas permite que os serviços de método de entrada os detectem. Portanto, você deve usar um dos teclados do aplicativo se quiser que este acionador funcione.
+ Muitos dedos para realizar o gesto devido às limitações do Android.
+ A duração do gesto é muito alta devido às limitações do Android.
+ Seus mapeamentos pararão de funcionar aleatoriamente!
+ Seus mapeamentos estão pausados!
+ Resumir
+ O serviço de acessibilidade precisa estar ativado para que seus mapeamentos funcionem!
+ Seu telefone finalizou o aplicativo quando estava em segundo plano ou ele travou!
+ O serviço de acessibilidade está ativado! Seus mapeamentos devem funcionar.
+ O registro extra está ativado! Desative isso se você não estiver tentando corrigir um problema.
+ Desligar
+ Sobre
+ Abrir %s
+ Pressiona a tecla \'%s\'
+ Digita \'%s\'
+ Inserir %s%s
+ Inserir %s através do terminal
+ Inserir %s%s de %s
+ Abrir %s
+ Tocar nas coordenadas %d, %d
+ Tocar nas coordenadas %d, %d (%s)
+ Deslizar com %d dedo(s) das coordenadas %d/%d para %d/%d em %dms
+ Deslizar com %d dedo(s) das coordenadas %d/%d para %d/%d em %dms (%s)
+ %s com %d dedo(s) nas coordenadas %d/%d com uma distância de pinçamento de %dpx em %dms
+ %s com %d dedo(s) nas coordenadas %d/%d com uma distância de pinçamento de %dpx em %dms (%s)
+ Ligar para %s
+ Toca o som: %s
+ Opções
+ Ações
+ Gatilho
+ Restrições
+ Deslizar para cima
+ Deslizar para baixo
+ Deslizar para a esquerda
+ Deslizar para a direita
+ Tipo de clique
+ Extras
+ Iniciar X
+ Iniciar Y
+ Finalizar X
+ Finalizar Y
+ Distância de pinçamento (px)
+ Tipo de pinçamento
+ Pinçar para dentro
+ Pinçar para fora
+ Código da chave
+ Do dispositivo
+ Nome do atalho
+ Descrição coordenada (opcional)
+ Entrada de texto
+ Abrir URL
+ Discar número
+ Ação
+ Categorias
+ Dados
+ Pacote
+ Classe
+ Nome
+ Valor (%s)
+ Descrição para Mapeador de Teclas (obrigatório)
+ Bandeiras
+ Descrição do Arquivo de Áudio
+ Mostrar SSID da rede WiFi
+ Ao mesmo tempo
+ Na sequencia
+ E
+ OU
+ Pressionar por curto período
+ Pressione por mais tempo
+ Pressione duas vezes
+ Confirmado
+ Negado
+ Atividade
+ Serviço
+ Receptor de Transmissão
+ Gatilho e ações
+ Eventos e mais
+ Opções
+ Restrições
+ Ações
+ Gatilho
+ Digital
+ Escolha %s
+ “Backup” concluído!
+ Backup falhou!
+ Restauração bem sucedida!
+ A restauração falhou!
+ Backup automático concluído!
+ Falha ao realizar o backup automático!
+ Captura feita
+ Erro de IO ¯\\_(ツ)_/¯
+ A resolução da captura de tela não corresponde à resolução deste dispositivo!
+ Copiou UUID para clipboard
+ Gatilho acionado
+ Copiou registro
+ Root ativado!
+ Falha selecionando som.
+ Sem sons salvos!
+ Key Mapper usou Shizuku para permitir WRITE_SECURE_SETTINGS
+ Key Mapper usou root para permitir WRITE_SECURE_SETTINGS
+ Expiração do gatilho (ms)
+ Atraso do toque longo (ms)
+ Expiração toque duplo (ms)
+ Atraso repetição (ms)
+ Limite repetição
+ Repetir a cada... (ms)
+ Duração da vibração (ms)
+ Quantas vezes?
+ Quantas repetições?
+ Atraso antes da próxima ação (ms)
+ Duração de pressão (ms)
+ Duração do deslize (ms)
+ Número de dedos
+ Coordenadas para definir com captura de tela
+ Iniciar
+ Finalizar
+ Duração do pinça (ms)
+ Número de dedos
+ %s está em primeiro plano
+ %s não está visível
+ %s está reproduzindo mídia
+ %s não está reproduzindo mídia
+ %s está conectado
+ %s está desconectado
+ A tela está ligada
+ A tela está desligada
+ A lanterna de %s está desligada
+ A lanterna de %s está ligada
+ e
+ ou
+ Aplicativo
+ Bluetooth
+ Tela
+ Orientação
+ Aplicativo em primeiro plano
+ Aplicativo não está em primeiro plano
+ Dispositivo Bluetooth conectado
+ Dispositivo Bluetooth desconectado
+ A tela está ligada
+ A tela está desligada
+ Retrato (0°)
+ Paisagem (90°)
+ Retrato (180°)
+ Paisagem (270°)
+ Retrato (qualquer orientação)
+ Paisagem (qualquer orientação)
+ Aplicativo reproduzindo mídia
+ Aplicativo não reproduzindo mídia
+ Mídia em reprodução
+ Nenhuma mídia em reprodução
+ Lanterna ligada
+ Lanterna desligada
+ Wi-Fi ativado
+ Wi-Fi desativado
+ Conectado à rede Wi-Fi
+ Desconectado da rede Wi-Fi
+ Você precisará digitar o SSID manualmente, pois aplicativos não têm permissão para consultar a lista de redes Wi-Fi conhecidas no Android 10 e versões posteriores.
+
+ Deixe em branco para corresponder a qualquer rede Wi-Fi.
+ Qualquer
+ Conectado a rede Wi-Fi: %s
+ Desconectado da rede Wi-Fi: %s
+ Conectado à qualquer rede Wi-Fi
+ Desconectado, sem rede Wi-Fi
+ Método de entrada selecionado
+ %s foi selecionado
+ Método de entrada não selecionado
+ %s Não escolhido
+ O Dispositivo esta bloqueado
+ Este dispositivo está desbloqueado
+ Chamada telefônica
+ Fora de chamada
+ Tocando
+ Carregando
+ Descarregando
+ Retrato (0°)
+ Paisagem (90°)
+ Retrato (180°)
+ Paisagem (270°)
+ Pressionar por curto período
+ Pressione por mais tempo
+ Pressione duas vezes
+ Selecionar ação
+ Desativa bloqueio de app
+ Veja dontkillmyapp.com
+ \n\nApós ler vá para o próximo slide.
+ Guia do usuário
+ Reiniciar o serviço de acessibilidade
+ Desative e ative o serviço de acessibilidade.
+ Reiniciar
+ Reportar um erro
+ Escolha o local tocando em \"criar relatório\". A seguir veja como enviar.
+ Criar relatório
+ Compartilhar relatório de erros
+ Você pode enviar via Discord ou GitHub. Anexe o erro na mensagem!
+ Discord
+ GitHub
+ Configurações
+ Concluído
+ Selecionar tudo
+ Sobre
+ Pesquisa
+ Guia rápido de início
+ Ajuda
+ Habilitar
+ Desativar
+ Desativar tudo
+ Habilitar tudo
+ Reportar erro
+ Mostrar método de entrada
+ Adicionar dados
+ Salvar
+ Duplicar
+ Backup
+ Restaurar
+ Salvar tudo
+ Salvar geral
+ Toque e pause
+ Toque para retomar
+ Redefinir
+ Salvar
+ Alternar mensagens curtas
+ Copiar
+ Limpar
+ Adicionar ação
+ Gravação de tecla
+ Concluído
+ Salvar
+ Corrigir
+ %d…
+ Adicionar restrição
+ Selecionar código de tecla
+ Sim!
+ Selecionar ação
+ Adicionar extra
+ Criar atalho de acesso rápido
+ Crie o atalho manualmente
+ Guia de Intenção
+ Ajuda
+ Selecionar captura de tela (opcional)
+ Escolher atividade
+ Definir bandeiras
+ Copiar
+ Sem limite
+ Escolher arquivo de som
+ Editar ação
+ Substituir ação
+ Sim?
+ Criar título de atalho
+ Permissão de super usuário necessária!
+ Mais
+ Escolha o fluxo
+ Escolha o flash
+ Não consigo encontrar a página de configurações de acessibilidade
+ Não é possível gravar o gatilho?
+ Descartar suas alterações
+ Tem certeza de que deseja descartar suas alterações?
+ Se você sabe que seu telefone não está enraizado ou não sabe o que é root, não poderá usar recursos que funcionam apenas em dispositivos enraizados. Ao tocar em ‘OK’, você será levado às configurações.
+ Nas configurações, role até a parte inferior e toque em \'Key Mapper tem permissão de root\' para que você possa usar recursos/ações de root.
+ Baixando…
+ Pressionar por muito tempo só funciona para botões físicos de volume e navegação. Se você habilitar para outras teclas, as teclas não funcionarão quando não forem pressionadas por muito tempo.
+ Conceder permissão WRITE_SECURE_SETTINGS
+ Um PC/Mac é necessário para conceder esta permissão. Leia o guia online.
+ Seu dispositivo não parece ter uma página de configurações de serviços de acessibilidade. Toque em \"guia\" para ler o guia online que explica como consertar isso.
+ "Não é possível pressionar várias teclas duas vezes simultaneamente."
+ As teclas precisam ser listadas de cima para baixo na ordem em que serão pressionadas.
+ Um gatilho de \"sequência\" tem um tempo limite diferente dos gatilhos paralelos. Isso significa que após pressionar a primeira tecla, você terá um tempo definido para inserir o restante das teclas no gatilho. Todas as teclas que.
+ O Android não permite que aplicativos vejam dispositivos Bluetooth não emparelhados. Eles só detectam conexão/desconexão. Se o dispositivo Bluetooth já estiver emparelhado ao iniciar o serviço de acessibilidade, reconecte-o para o aplicativo reconhecer.
+ Alterar local ou desativar o backup automático?
+ As restrições de tela ligada/desligada só funcionarão se você tiver ativado a opção de mapa de teclas \"detectar gatilho quando a tela estiver desligada\". Esta opção só será exibida para algumas teclas (por exemplo, botões de volume) e se você tiver root
+ Se você tiver qualquer outro tipo de bloqueio de tela escolhido, como PIN ou padrão, então você não precisa se preocupar. Mas se você tiver um bloqueio de tela por senha, você NÃO conseguirá desbloquear seu telefone se usar o Método de Entrada Básico do Key Mapper, porque ele não possui uma interface gráfica. Você pode conceder ao Key Mapper a permissão WRITE_SECURE_SETTINGS para que ele possa mostrar uma notificação para alternar entre o teclado e o bloqueio de tela. Há um guia sobre como fazer isso se você tocar no ponto de interrogação na parte inferior da tela.
+ Selecione o método de entrada para ações que exigem um. Você pode alterar isso mais tarde tocando em \"Selecionar teclado para ações\" no menu inferior da tela inicial.
+ Você precisa escolher o layout de teclado \"Caps Lock para câmera\" para o seu teclado; caso contrário, a tecla Caps Lock ainda bloqueará as letras maiúsculas. Você pode encontrar essa configuração em configurações do dispositivo -> Idiomas e entrada -> Teclado físico -> Toque no seu teclado -> Configurar layouts de teclado. Isso irá remapear a tecla Caps Lock para KEYCODE_CAMERA, permitindo que o Key Mapper a remapeie corretamente.\n\nDepois de fazer isso, você deve remover a tecla de ativação Caps Lock e gravar a tecla Caps Lock novamente. Deve aparecer \"Câmera\" em vez de \"Caps Lock\" se você seguiu os passos corretamente.
+ Reinicie seu dispositivo se o botão \"Gravar gatilho\" estiver contando regressivamente e os botões que você está pressionando não estiverem aparecendo. Se seus botões ainda não aparecerem após reiniciar, então o Key Mapper não suporta seus botões. Não há uma solução para isso.
+ Nenhum dispositivo externo conectado.
+ Instale o Teclado GUI do Key Mapper.
+ Isto é altamente recomendado! Este é um teclado adequado que você pode usar com o Key Mapper. O que vem embutido no Key Mapper (o Método de Entrada Básico) não possui teclado na tela. Escolha de onde deseja instalá-lo.
+ Instale o Teclado Leanback do Key Mapper.
+ Isto é altamente recomendado! Este é um teclado adequado para Android TV que você pode usar com o Key Mapper. O que vem embutido no Key Mapper (o Método de Entrada Básico) não possui teclado na tela. Escolha de onde deseja instalá-lo.
+ Instale o Teclado GUI do Key Mapper.
+ Escolha de onde deseja baixá-lo.
+ Instale o Teclado Leanback do Key Mapper.
+ Escolha de onde deseja baixá-lo.
+ Esta ação precisa de algumas configurações extras.
+ Existem 3 maneiras de configurar seu dispositivo para usar esta ação. Aqui estão as vantagens e desvantagens de cada uma.
+
+ \n\n1. Baixe o Shizuku (recomendado). Você não precisa usar um teclado na tela diferente do que já está usando, mas isso exigirá um minuto de configuração toda vez que você reiniciar seu dispositivo.
+
+ \n\n2. Baixe o Teclado GUI do Key Mapper. Este é um teclado na tela que você pode usar com o Key Mapper, mas não poderá usar o teclado que está atualmente em uso, como o Gboard.
+
+ \n\n3. Não faça nada e use o teclado embutido do Key Mapper. Isso não é recomendado, pois você não terá nenhum teclado na tela ao usar o Key Mapper! Não há vantagens.
+ Esta ação precisa de algumas configurações extras.
+ Existem 3 maneiras de configurar seu dispositivo para usar esta ação. Aqui estão as vantagens e desvantagens de cada uma.
+
+ \n\n1. Baixe o Shizuku (recomendado). Você não precisa usar um teclado na tela diferente do que já está usando, mas isso exigirá um minuto de configuração toda vez que você reiniciar seu dispositivo.
+
+ \n\n2. Baixe o Teclado Leanback do Key Mapper. Este é um teclado na tela otimizado para Android TV que você pode usar com o Key Mapper, mas não poderá usar o teclado que está atualmente em uso, como o Gboard.
+
+ \n\n3. Não faça nada e use o teclado embutido do Key Mapper. Isso não é recomendado, pois você não terá nenhum teclado na tela ao usar o Key Mapper! Não há vantagens.
+ Desativar otimização de bateria
+ Você DEVE ler tudo isso senão você ficará frustrado no futuro!\n\nTocar em \"corrigir parcialmente\" pode impedir que o Android pare o aplicativo enquanto ele está em segundo plano.\n\nISSO NÃO É SUFICIENTE. A interface do seu OEM, como MIUI ou Samsung Experience, pode ter outros recursos de encerramento de aplicativos, então você DEVE desativá-los para o Key Mapper também, seguindo o guia online em dontkillmyapp.com.
+ Enviar feedback
+ Leia o guia sobre como relatar problemas no site.
+ Ative o serviço de acessibilidade para que você possa gravar um gatilho.
+ Reinicie o serviço de acessibilidade desligando-o e ligando-o novamente para que você possa gravar um gatilho.
+ Ative o serviço de acessibilidade para que você possa testar a ação.
+ Reinicie o serviço de acessibilidade desligando-o e ligando-o novamente para que você possa testar a ação.
+ Reinicie o serviço de acessibilidade desligando-o e ligando-o novamente.
+ Usar este gatilho pode causar uma tela preta quando você desbloqueia seu dispositivo após usar a configuração de fixação de tela nas configurações do seu dispositivo. Isso pode ser corrigido com uma reinicialização. Isso não acontece em todos os dispositivos, então fique atento e desative a configuração se isso ocorrer!
+ Redefinir mapas de gestos de impressão digital
+ Você tem certeza de que deseja redefinir seus mapas de gestos de impressão digital?
+ O Key Mapper foi fechado ou travou
+ É muito provável que seu telefone tenha encerrado o Key Mapper enquanto ele tentava rodar em segundo plano. Isso é não é culpa do desenvolvedor e não há nada que eles possam fazer para corrigir isso, então, por favor, não deixe uma avaliação negativa 😃.
+
+ \n\nVocê já seguiu o guia em dontkillmyapp.com antes para impedir que seu telefone mate o Key Mapper?
+ Sim
+ Não
+ Erro ao gerar relatório de erros
+ Solicitar permissão Shizuku
+ Como você está usando o Shizuku, é altamente recomendável conceder essa permissão, pois alguns recursos do Key Mapper podem ser feitos sem que você precise configurar nada (por exemplo, inserir códigos de chave sem nós).
+ Corrigir erro
+ É necessária permissão
+ O Key Mapper precisa da permissão \"dispositivos próximos\" para poder obter a lista de dispositivos Bluetooth pareados.
+ Você não tem nenhum aplicativo de arquivos instalado que permita criar um arquivo para o Key Mapper. Por favor, instale um gerenciador de arquivos.
+ Você não tem nenhum aplicativo de arquivos instalado que permita escolher um arquivo para o Key Mapper. Por favor, instale um gerenciador de arquivos.
+ O serviço de acessibilidade deve estar habilitado
+ @string/accessibility_service_explanation
+ Conceder acesso Não Perturbe
+ Você será levado à página de configurações do seu dispositivo para gerenciar quais aplicativos podem modificar o estado de Não Perturbe. Isso não está presente em alguns dispositivos, então toque em não mostrar novamente se você não vir o Key Mapper na lista.
+ Sim
+ Confirmar
+ Concluído
+ Assinar
+ Guia
+ Guia
+ Guia
+ Habilitar recursos root
+ Conceder
+ Participar
+ Alterar
+ Corrigir parcialmente
+ Ok
+ Ligar
+ Reiniciar
+ Nunca mais mostrar
+ Abrir guia online
+ Desligar
+ Fique fora
+ Não
+ Cancelar
+ Não mostrar novamente
+ Guia on-line
+ Configurações
+ Documentação
+ O que mudou
+ Reportar erro
+ Reiniciar
+ Ir para o guia
+ Shizuku
+ Teclado GUI do Mapeador de Teclas
+ Teclado Leanback do Mapeador de Teclas
+ Não faça nada
+ Corrigir
+ Cancelar
+ Corrigir
+ Seletor de teclado
+ Pausar/retomar mapeamentos
+ Aviso de teclado oculto
+ Teclado Toggle Key Mapper
+ Novas funcionalidades
+ Toque para trocar teclado.
+ Seletor de teclado
+ Em execução
+ Toque para abrir o Key Mapper.
+ Pausar
+ Pausado
+ Toque para abrir o Key Mapper.
+ Retomar
+ Descartar
+ Reiniciar
+ O serviço de acessibilidade está desativado
+ Iniciar o serviço de acessibilidade.
+ O serviço de acessibilidade precisa ser reiniciado!
+ O serviço de acessibilidade travou! Seu telefone pode estar matando-o agressivamente! Toque para reiniciar o serviço de acessibilidade.
+ Parar serviço
+ O teclado está escondido!
+ Toque em \'mostrar teclado\' para começar a mostrar o teclado novamente.
+ Teclado Toggle Key Mapper
+ Toque em \"alternar\" para alternar entre o teclado do Key Mapper.
+ Alternar
+ Remapeie os gestos de impressão digital com o Key Mapper!
+ Seu dispositivo suporta remapeamento de swipes no sensor de impressão digital. Toque para começar a remapeamento!
+ Você precisa configurar algumas coisas novamente!
+ Parece que você estava usando a configuração para mudar automaticamente o teclado ou mostrar o seletor de métodos de entrada quando um dispositivo Bluetooth se conecta ou desconecta. O Key Mapper agora permite que você use qualquer dispositivo de entrada e não apenas dispositivos Bluetooth. Não há uma maneira de migrar as configurações antigas de forma que a nova funcionalidade funcione, então você terá que escolher os dispositivos novamente nas configurações do Key Mapper. Toque para abrir o Key Mapper.
+ Atraso de pressão longa padrão (ms)
+ Por quanto tempo um botão deve ser pressionado para ser detectado como um pressionamento longo. O padrão é 500 ms. Pode ser substituído nas opções de um mapa de teclas.
+ Duração padrão do pressionamento duplo (ms)
+ Quão rápido um botão precisa ser pressionado duas vezes para ser detectado como um pressionamento duplo. O padrão é 300 ms. Pode ser substituído nas opções de um mapa de teclas.
+ Por quanto tempo vibrar se a vibração estiver habilitada para um mapa de teclas. O padrão é 200 ms. Pode ser substituído nas opções de um mapa de teclas.
+ Vibração padrão (ms)
+ Quanto tempo o gatilho precisa ser mantido pressionado para que a ação comece a se repetir. O padrão é 400 ms. Pode ser substituído nas opções de um mapa de teclas.
+ Atraso repetição (ms)
+ O atraso entre cada vez que uma ação é repetida. O padrão é 50 ms. Pode ser substituído nas opções de um mapa de teclas.
+ Atraso repetição (ms)
+ O tempo permitido para completar um gatilho de sequência. O padrão é 1000 ms. Pode ser substituído nas opções de um mapa de teclas.
+ Expiração do gatilho (ms)
+ Redefinir
+ Força todos os mapas de teclas a vibrar.
+ Força vibrar
+ Notificação do Teclado
+ Exiba uma notificação persistente para permitir que você escolha um teclado.
+ Notificação de pausa/retomada de mapeamentos
+ Exiba uma notificação persistente que inicia/pausa seus mapeamentos.
+ Fazer backup automático de mapeamentos para um local especificado
+ Nenhum local escolhido.
+ Escolha dispositivos
+ Mostrar seletor de teclado automaticamente
+ Quando um dispositivo que você escolheu se conecta ou desconecta, o seletor de teclado será exibido automaticamente. Escolha os dispositivos abaixo.
+ Alterar automaticamente o teclado na tela quando um dispositivo (por exemplo, um teclado) conecta/desconecta
+ O último teclado Key Mapper usado será automaticamente selecionado quando um dispositivo escolhido for conectado. Seu teclado normal será automaticamente selecionado quando o dispositivo for desconectado.
+ Alterar automaticamente o teclado na tela quando você começar a inserir texto
+ O último teclado não Key Mapper usado será selecionado automaticamente quando você tentar abrir o teclado. Seu teclado Key Mapper será selecionado automaticamente quando você parar de usá-lo.
+ Mostrar uma mensagem na tela ao alterar o teclado automaticamente
+ O Key Mapper tem permissão de root
+ Ative isso se você quiser usar recursos/ações que funcionam apenas em dispositivos com root. O Key Mapper deve ter permissão de root do seu aplicativo de gerenciamento de acesso root (por exemplo, Magisk, SuperSU) para que esses recursos funcionem.
+ Ative isso apenas se você souber que seu dispositivo está rooteado e que você concedeu permissão de root ao Key Mapper.
+ Tema escuro
+ Configurações de notificação
+ Alterne entre o teclado do Key Mapper e o teclado padrão ao tocar na notificação.
+ Alternar notificação de teclado do Key Mapper
+ Alterar automaticamente o teclado ao alternar os mapas de teclas
+ Selecione automaticamente o teclado do Key Mapper ao retomar seus mapas de teclas e selecione seu teclado padrão ao pausá-los.
+ Ocultar alertas da tela inicial
+ Oculte os alertas na parte superior da tela inicial.
+ Mostrar os primeiros 5 caracteres do ID do dispositivo para gatilhos específicos do dispositivo
+ Isso é útil para diferenciar entre dispositivos que têm o mesmo nome.
+ Corrigir teclados que estão configurados para inglês dos EUA
+ Isso corrige teclados que não têm o layout de teclado correto quando um serviço de acessibilidade está habilitado. Toque para ler mais e configurar.
+ Corrigir teclados que estão configurados para inglês dos EUA
+ Há um bug no Android 11 que ao ativar um serviço de acessibilidade faz o Android pensar que todos os dispositivos externos são o mesmo dispositivo virtual interno. Como ele não consegue identificar esses dispositivos corretamente,
+ Escolha os dispositivos
+ Instalar o teclado GUI do Key Mapper (opcional)
+ Instale o teclado Key Mapper Leanback (opcional)
+ Habilitar o teclado GUI do Key Mapper ou o método de entrada básico do Key Mapper
+ Habilitar o teclado Leanback do Key Mapper ou o método de entrada básico do Key Mapper
+ Use o teclado que você acabou de habilitar
+ (Recomendado) Leia o guia do usuário para esta configuração.
+ Ativar o registro de depuração extra
+ Visualizar e compartilhar
+ Reportar problema
+ Apagar som #
+ Exclua arquivos de som que podem ser usados para a ação Som.
+ Conceder permissão
+ Permissão concedida
+ 1. Shizuku não está instalado! Toque para baixar o aplicativo Shizuku.
+ 1. Shizuku está instalado.
+ 2. Shizuku não foi iniciado! Toque para abrir o aplicativo Shizuku e então leia as instruções que explicam como iniciá-lo.
+ 2. Shizuku iniciado.
+ O Key Mapper não tem permissão para usar Shizuku. Toque para conceder essa permissão.
+ O Key Mapper usará Shizuku automaticamente. Toque para ler quais recursos do Key Mapper usam Shizuku.
+ Opções de mapeamento padrão
+ Altere as opções padrão para seus mapeamentos.
+ Mostrar automaticamente o seletor de teclado
+ Toque para ver as configurações que permitem mostrar automaticamente o seletor de teclado.
+ Notificações
+ Padrões
+ Configurações de root
+ Essas opções só funcionarão em dispositivos root! Se você não sabe o que é root ou se seu dispositivo tem root, não deixe uma avaliação ruim se elas não funcionarem. :)
+ Requer permissão WRITE_SECURE_SETTINGS
+ Essas opções só são habilitadas se o Key Mapper tiver a permissão WRITE_SECURE_SETTINGS. Clique no botão abaixo para saber como conceder a permissão.
+ Suporte Shizuku
+ Shizuku é um aplicativo que permite que o Key Mapper faça coisas que somente aplicativos de sistema podem fazer. Você não precisa usar o teclado do Key Mapper, por exemplo. Toque para aprender como configurar isso.
+ Siga estes passos para configurar o Shizuku.
+ Alterar o teclado automaticamente
+ Essas são configurações muito úteis e é recomendável que você as confira!
+ Registros
+ Isso pode adicionar latência aos seus mapas de teclas, portanto, ative-o somente se estiver tentando depurar o aplicativo ou se o desenvolvedor tiver solicitado.
+ O que mudou
+ Licença
+ Política de privacidade
+ Créditos
+ Código-fonte
+ Perfil do desenvolvedor no GitHub
+ Avalie e comente
+ Tópico XDA
+ Versão
+ Traduzir
+ Servidor Discord
+ Canal do YouTube (Tutoriais)
+ Mostrar caixa de diálogo de volume
+ Pressione por mais tempo
+ Vibrar
+ Mostrar uma mensagem na tela
+ Vibrar quando as teclas são pressionadas pela primeira vez e novamente quando pressionadas por muito tempo.
+ Detectar gatilho quando a tela estiver desligada
+ Repita
+ %dx
+ depois de %dms
+ todo %dms
+ até ser deslizado novamente
+ até ser pressionado novamente
+ até ser liberado
+ Repita
+ Repita até liberar
+ Repita até ser pressionado novamente
+ Segure firme
+ Mantenha pressionado até ser pressionado novamente
+ Não remapeie
+ Mantenha pressionado até deslizar novamente
+ Permitir que outros aplicativos acionem este mapa de teclas
+ O serviço de acessibilidade está ativado :)
+ Tudo bem. O Key Mapper agora pode detectar seus pressionamentos de botão.
+ Reiniciar o serviço de acessibilidade
+ Os serviços de acessibilidade estão habilitados, mas foram encerrados pelo seu telefone ou travaram. Reinicie-o.
+ Reiniciar
+ Ativar serviço de acessibilidade
+ @string/accessibility_service_explanation
+ O serviço de acessibilidade está desativado
+ Você só pode gravar um gatilho se o serviço de acessibilidade estiver habilitado.
+ Diretamente dos {developers}
+ Não há garantia de que cada ação funcionará no seu dispositivo e que cada botão pode ser detectado. Isso ocorre porque existem muitas versões do Android, e os fabricantes podem quebrar recursos acidentalmente ou intencionalmente. Se algo não funcionar, por favor, notifique o desenvolvedor e evite dar uma avaliação negativa ao aplicativo, pois o problema muitas vezes está fora do controle do desenvolvedor. =)
+ O Key Mapper pode parar de funcionar aleatoriamente!
+ CRÍTICO!!! Toque em \"desligar\" para, com sorte, impedir que o Android pare o aplicativo enquanto ele estiver em segundo plano. A skin do seu OEM, como MIUI ou Samsung Experience, pode ter outros recursos de \"economia de bateria\"
+ A otimização de bateria do Android Stock está desativada. Isso não é bom o suficiente para a maioria dos dispositivos, então vá para dontkillmyapp.com para tutoriais que mostram como desativar ainda mais recursos que matam aplicativos no seu dispositivo.
+ Desligar
+ Acesse dontkillmyapp.com
+ Remapeando botões de volume?
+ Você pode remapear os botões de volume
+ O Key Mapper precisa do acesso Não Perturbe se você quiser que ações que alterem o volume e botões de volume remapeados funcionem.
+ Tudo certo
+ Contribuindo
+ "Este aplicativo é de código aberto! Você pode começar a contribuir indo para o repositório sds100/KeyMapper no GitHub e entrando no servidor Discord. Mesmo que você não saiba programar, você pode contribuir ajudando outros"
+ Toque nos 3 pontos para alterar o comportamento de repetição e mais. Você pode testar uma ação e corrigir erros de ação tocando nela.
+ Remapeando gestos do leitor de impressão digital
+ Você pode remapear os gestos do leitor de impressão digital! :)
+ Você não pode remapear os gestos do leitor de impressão digital!
+ Você precisará habilitar o serviço de acessibilidade para que o Key Mapper possa verificar se o seu dispositivo consegue detectar gestos de impressão digital.
+ Seu dispositivo pode detectar gestos de impressão digital! Há uma aba no topo da tela inicial para remapear gestos de impressão digital.
+ Seu dispositivo não permite que aplicativos de terceiros detectem gestos de impressão digital! Não há nada que o desenvolvedor possa fazer sobre isso. Alguns dispositivos têm a configuração para deslizar para baixo no leitor de impressão digital para abrir a bandeja de notificações e não permitem que aplicativos de terceiros detectem gestos de impressão digital.
+ Habilitar
+ Você precisa configurar algumas configurações novamente
+ Parece que você estava usando a configuração para mudar automaticamente o teclado ou mostrar o seletor de métodos de entrada quando um dispositivo Bluetooth se conecta ou desconecta. O Key Mapper agora permite que você use qualquer dispositivo de entrada e não apenas dispositivos Bluetooth. Não há uma maneira de migrar as configurações antigas de forma que a nova funcionalidade funcione, então você terá que escolher os dispositivos novamente.
+ Conceda permissão a Shizuku
+ Parece que você tem o Shizuku instalado. É recomendado conceder ao Key Mapper permissão para usar o Shizuku, para que o Key Mapper possa fazer mais coisas sem a intervenção do usuário. Por exemplo, registrar pressionamentos de botões sem que você precise usar o \'teclado do Key Mapper\'. Toque em \'mais informações\' para ler todos os benefícios. Toque em \'conceder\' para dar a permissão.
+ Shizuku permissão concedida!
+ Você concedeu permissão ao Key Mapper Shizuku com sucesso.
+ Mais informação
+ Conceder
+ Shizuku não foi iniciado
+ O Shizuku deve ser iniciado antes que você conceda permissão ao Key Mapper para usá-lo. Toque em \'Launch Shizuku\' para abrir o aplicativo Shizuku para que você possa iniciá-lo.
+ Iniciar Shizuku
+ Conceder permissão de notificação
+ Alguns recursos do Key Mapper exigem permissão para postar notificações, por exemplo, há uma notificação para pausar/retomar seus mapas de teclas. Toque em \"conceder\" para dar permissão.
+ Notificações podem ser exibidas!
+ Você concedeu com sucesso permissão ao Key Mapper para mostrar notificações.
+ Conceder
+ Acessibilidade
+ Alarme
+ DTMF
+ Música
+ Notificações
+ Anel de Transmissão
+ Sistema
+ Chamada de voz
+ Modo de toque normal
+ Vibrar
+ Silencioso
+ Frente
+ Voltar
+ Alarmes
+ Prioridade
+ Nada
+ Pausar mapeamentos
+ Retomar mapeamentos
+ Serviço desativado
+ O serviço de acessibilidade do Key Mapper está desabilitado
+ O serviço de teclado Key Mapper está desabilitado
+ Teclado Toggle Key Mapper
+ Ctrl
+ Ctrl+Esquerda
+ Ctrl+Direita
+ Alt
+ Alt + Esquerda
+ Alt+Direita
+ Shift
+ Shift+Seta para a esquerda
+ ~Deslocar para a direita
+ Meta
+ Meta esquerda
+ Meta direita
+ Sym
+ Func
+ Caps Lock
+ Num Lock
+ Scroll Lock
+ Você deve digitar uma chave!
+ A regra deve ter pelo menos um gatilho
+ Você precisa escolher uma ação!
+ O atalho deve ter um título!
+ Você deve estar usando um dos teclados do Key Mapper para que esta ação funcione!
+ Não é possível encontrar o atalho. O aplicativo está instalado ou habilitado?
+ O aplicativo com nome de pacote %s não está instalado!
+ A aplicação não está instalada!
+ Aplicativo Desativado!
+ Aplicativo %s desativado!
+ Você precisa conceder permissão ao Key Mapper para modificar as configurações do sistema.
+ Isso requer permissão de root!
+ Esta ação requer permissão da câmera!
+ Requer Android %s ou posterior
+ Requer Android %s ou posterior
+ Seu dispositivo não possui uma câmera.
+ Este dispositivo não oferece suporte ao NFC.
+ Seu dispositivo não possui um Sensor de Impressão Digital.
+ Este dispositivo não suporta wifi.
+ Seu dispositivo não suporta Bluetooth.
+ Seu dispositivo não oferece suporte à aplicação de políticas de dispositivo.
+ Seu dispositivo não possui uma câmera.
+ Seu dispositivo não possui nenhum recurso de telefonia.
+ Classe \"%s\" não encontrada!
+ Não é possível encontrar a página de configurações do teclado!
+ O Key Mapper precisa ser um administrador de dispositivo!
+ No modo Não Perturbe!
+ O Key Mapper não tem permissão para usar esse atalho
+ O aplicativo precisa de permissão para alterar o estado Não Perturbe!
+ Esta ação precisa de permissão para ler o estado do telefone!
+ Não é possível encontrar a página de permissão WRITE_SETTINGS!
+ Erro ao abrir este atalho de aplicativo
+ Não há nenhum aplicativo instalado que possa enviar e-mails!
+ Permissão para alterar o modo Não Perturbe concedida!
+ Não é possível encontrar as configurações de permissão de acesso Não Perturbe!
+ O Key Mapper precisa da permissão WRITE_SECURE_SETTINGS.
+ Nenhum aplicativo pode ser encontrado para abrir esse URL
+ Falha ao executar o comando \"getevent\". Tem certeza de que está rooteado?
+ Não há nenhum aplicativo que possa iniciar esta chamada telefônica
+ A câmera está em uso!
+ Câmera desconectada!
+ Câmera desativada!
+ Erro de câmera!
+ Número máximo de câmeras em uso!
+ Não é possível acessar a câmera!
+ Sem flash frontal
+ Sem flash traseiro
+ O serviço de acessibilidade deve estar habilitado para que este aplicativo funcione!
+ O serviço de acessibilidade precisa ser habilitado!
+ O serviço de acessibilidade está ativado!
+ O serviço de acessibilidade precisa ser habilitado!
+ O serviço de acessibilidade precisa ser reiniciado!
+ Seu inicializador não suporta atalhos.
+ Alguns recursos precisam da permissão WRITE_SECURE_SETTINGS.
+ Permissão WRITE_SECURE_SETTINGS concedida.
+ O método de entrada selecionado precisa ser habilitado para “Evento de tecla”, “Tecla”, “Texto” e algumas outras ações para funcionar.
+ Um teclado Key Mapper precisa ser habilitado!
+ Um teclado Key Mapper está habilitado!
+ Um teclado Key Mapper deve estar habilitado e selecionado para que algumas de suas ações funcionem!
+ Não é possível encontrar o método de entrada %s
+ O seletor de método de entrada não pode ser exibido!
+ Falha ao encontrar o elemento de acessibilidade!
+ Falha ao executar a ação global %s!
+ Seus mapas de teclas não funcionam! Algumas coisas precisam ser consertadas!
+ Você não pode ir para o final do texto neste campo!
+ Configurações de otimização de bateria não encontradas! Se existir, abra manualmente.
+ Extra (%s) não encontrado!
+ Você não pode ter restrições duplicadas!
+ Esse gesto já tem essa restrição!
+ Não pode estar vazio!
+ Isso não é suportado. :(
+ Dispositivo não encontrado!
+ Falha ao selecionar o arquivo
+ Arquivo JSON vazio!
+ Acesso ao arquivo negado! %s
+ Erro de I/O desconhecido!
+ Cancelado!
+ Falha ao fazer backup do arquivo. Ele foi excluído?
+ Número inválido!
+ Deve ser pelo menos %s!
+ Deve ser no máximo %s!
+ A otimização da bateria está ativada! Desative isso porque isso pode fazer com que o Key Mapper pare de funcionar aleatoriamente.
+ A otimização da bateria está desativada!
+ Não é possível encontrar as configurações de acesso à notificação!
+ Permissão de acesso à notificação negada!
+ Inválido!
+ Permissão negada para iniciar chamadas telefônicas!
+ Você precisará atualizar o Key Mapper para a versão mais recente para usar este backup.
+ Arquivo JSON corrompido!
+ Não há assistente de voz instalado!
+ Permissões insuficientes
+ Você só tem teclados Key Mapper instalados!
+ Não há aplicativos reproduzindo mídia!
+ Arquivo de origem não encontrado! %s
+ Arquivo de destino não encontrado! %s
+ Falha ao inserir gesto!
+ Falha ao modificar a configuração do sistema %s!
+ Você precisa habilitar %s!
+ Falha ao mudar o IME!
+ Seu dispositivo não tem aplicativo de câmera!
+ Seu dispositivo não tem assistente!
+ Seu dispositivo não tem aplicativo de configurações!
+ Nenhum aplicativo pode abrir esta URL!
+ Falha ao criar o arquivo!
+ Não é uma pasta! %s
+ Não é um arquivo! %s
+ Diretório não encontrado! %s
+ Não é possível encontrar o arquivo de som!
+ Permissão de armazenamento negada!
+ A origem e o destino não podem ser os mesmos!
+ Não há espaço restante no alvo! %s
+ Permissão Shizuku negada!
+ Shizuku não começou!
+ Este arquivo não tem nome!
+ Você deve conceder permissão ao Key Mapper para ver seus dispositivos Bluetooth pareados.
+ Negada permissão para ler localização precisa!
+ Permissão negada para atender e encerrar chamadas telefônicas!
+ Permissão negada para ver dispositivos Bluetooth pareados!
+ Permissão negada para mostrar notificações!
+ Devem ser 2 ou mais!
+ Deve ser %d ou menos!
+ Deve ser maior que 0!
+ Deve ser maior que 0!
+ Deve ser maior que 0!
+ Deve ser maior que 0!
+ Deve ser %d ou menos!
+ Alternar WiFi
+ Habilitar WiFi
+ Desativar WiFi
+ Alternar Bluetooth
+ Habilitar Bluetooth
+ Desativar Bluetooth
+ Aumentar o volume
+ Diminuir volume
+ Volume mudo
+ Alternar mudo
+ Reativar volume
+ Mostrar caixa de diálogo de volume
+ Aumentar o fluxo
+ Aumentar fluxo %s
+ Diminuir fluxo
+ Diminuir fluxo %s
+ Alternar entre os modos de toque (Toque, Vibrar, Silencioso)
+ Alternar entre os modos de toque (Toque, Vibrar)
+ Alterar modo de toque
+ Alterar para o modo %s
+ Alternar modo Não perturbe
+ Alternar somente o modo %s DND
+ Ativar o modo Não perturbe
+ Habilitar somente o modo %s DND
+ Desativar o modo Não perturbe
+ Habilitar rotação automática
+ Desativar rotação automática
+ Alternar rotação automática
+ Modo retrato
+ Modo paisagem
+ Mudar orientação
+ Percorrer as rotações
+ Percorrer %s rotações
+ Alternar dados móveis
+ Habilitar dados móveis
+ Desativar dados móveis
+ Alternar brilho automático
+ Desativar brilho automático
+ Habilitar brilho automático
+ Aumentar o brilho da tela
+ Diminuir o brilho da tela
+ Expandir gaveta de notificações
+ Alternar gaveta de notificações
+ Expandir configurações rápidas
+ Alternar gaveta de configurações rápidas
+ Recolher barra de status
+ Pausar reprodução de mídia
+ Pausar a reprodução de mídia para um aplicativo
+ Pausar mídia por %s
+ Retomar reprodução de mídia
+ Retomar a reprodução de mídia para um aplicativo
+ Retomar mídia para %s
+ Reproduzir/Pausar reprodução de mídia
+ Reproduzir/Pausar reprodução de mídia para um aplicativo
+ Reproduzir/Pausar mídia para %s
+ Próxima faixa
+ Próxima faixa para um aplicativo
+ Próxima faixa para %s
+ Faixa anterior
+ Faixa anterior para um aplicativo
+ Faixa anterior para %s
+ Avanço rápido
+ Avanço rápido para um aplicativo
+ Avanço rápido para %s
+ Nem todos os aplicativos de mídia suportam avanço rápido. Por exemplo, Google Play Music.
+ Rebobinar
+ Retroceder para um aplicativo
+ Rebobinar para %s
+ Nem todos os aplicativos de mídia suportam retrocesso. Por exemplo, Google Play Music.
+ Volte
+ Ir para casa
+ Abertura recente
+ Abrir menu
+ Alternar tela dividida
+ Ir para o último aplicativo. (Pressione duas vezes em recentes)
+ Alternar lanterna
+ Habilitar lanterna
+ Desativar lanterna
+ Alternar lanterna %s
+ Habilitar lanterna %s
+ Desativar lanterna %s
+ Habilitar NFC
+ Desativar NFC
+ Alternar NFC
+ Captura de tela
+ Iniciar assistente de voz
+ Assistente de dispositivo de inicialização
+ Abra a câmera
+ Dispositivo de bloqueio
+ Dispositivo de bloqueio seguro
+ Você só poderá fazer login novamente com seu PIN. O scanner de impressão digital e o desbloqueio facial serão desabilitados. Esta é a única maneira confiável que encontrei para bloquear dispositivos não rooteados antes do Android Pie 9
+ Dispositivo de dormir/acordar
+ Você deve ativar a opção para detectar o gatilho quando a tela estiver desligada!
+ Não faça nada
+ Mover o cursor para o final
+ Esta ação pode não funcionar conforme o esperado em alguns aplicativos.
+ Alternar teclado
+ Esta ação só funcionará se você tiver tocado em um campo de entrada onde o teclado deveria ser exibido.
+ Mostrar teclado
+ Ocultar teclado
+ Mostrar seletor de teclado
+ Trocar teclado
+ Mudar para %s
+ Corte
+ Copiar
+ Colar
+ Selecionar palavra no cursor
+ Abrir configurações
+ Mostrar menu de energia
+ Alternar modo avião
+ Habilitar modo avião
+ Desativar o modo avião
+ Iniciar aplicativo
+ Atalho para iniciar aplicativo
+ Código de chave de entrada
+ Evento de tecla de entrada
+ Tela de toque
+ Deslize a tela
+ Tela de pinça
+ Texto de entrada
+ Abrir URL
+ Enviar intenção
+ Iniciar chamada telefônica
+ Atender chamada telefônica
+ Terminar chamada telefônica
+ Tocar som
+ Descartar notificação mais recente
+ Descartar todas as notificações
+ Navegação
+ Volume
+ Mídia
+ Teclado
+ Aplicativos
+ Entrada
+ Câmera & som
+ Conectividade
+ Conteúdo
+ Interface
+ Telefone
+ Tela
+ Notificações
+ Booleano
+ Matriz booleana
+ Inteiro
+ Matriz de inteiros
+ String
+ Matriz de strings
+ Long
+ Mariz long
+ Byte
+ Matriz de bytes
+ Double
+ Matriz dupla
+ Char
+ Matriz de caracteres
+ Float
+ Matriz float
+ Short
+ Matriz short
+ Só pode ser \"true\" ou \"false\"
+ Uma lista separada por vírgulas de \"true\" e \"false\". Por exemplo, true, false, true
+ Um inteiro válido na linguagem de programação Java.
+ Uma lista separada por vírgulas de inteiros válidos na linguagem de programação Java. Por exemplo, 100.399
+ Uma lista separada por vírgulas. Por exemplo, categoria1, categoria2
+ Qualquer texto.
+ Uma lista de strings separadas por vírgulas. Por exemplo string1, string2
+ Um Long válido na linguagem de programação Java.
+ Uma lista separada por vírgulas de Longs válidos na linguagem de programação Java. Por exemplo, 102302234234234,399083423234429
+ Um Byte válido na linguagem de programação Java.
+ Uma lista separada por vírgulas de bytes válidos na linguagem de programação Java. Por exemplo, 123,3
+ Um Double válido na linguagem de programação Java.
+ Uma lista separada por vírgulas de Doubles válidos na linguagem de programação Java. Por exemplo 1.0,3.234
+ Um Char válido na linguagem de programação Java. Por exemplo, \'a\' ou \'b\'
+ Uma lista separada por vírgulas de Chars válidos na linguagem de programação Java. Por exemplo, a,b,c
+ Um Float válido na linguagem de programação Java. Por exemplo 3.145
+ Uma lista separada por vírgulas de Floats válidos na linguagem de programação Java. Por exemplo 1241.123
+ Um Short válido na linguagem de programação Java. Por exemplo 2342
+ Uma lista separada por vírgulas de Shorts válidos na linguagem de programação Java. Por exemplo 3242,12354
+ As flags para um Intent são armazenadas como flags de bits. Essas flags alteram a maneira como o Intent é tratado. Se isso estiver em branco para um Intent de Atividade, o Key Mapper usará FLAG_ACTIVITY_NEW_TASK por padrão. Para obter muito mais informações, toque em \'docs\' para ver a documentação dos desenvolvedores Android.
+ Guia rápido de início
+ Confira o Guia de Início Rápido se você estiver travado.
+ GitHub
+ Site
+ Traduções
+ Versão %s
+ Avaliar
+ O que mudou
+ Discord
+ Coisas chatas
+ Licença
+ A licença de código aberto para este aplicativo.
+ Política de Privacidade
+ Não coletamos nenhuma informação pessoal, mas aqui está uma política de privacidade dizendo isso.
+ Nossa equipe
+ Desenvolvedor
+ Moderador/suporte da comunidade
+ Moderador/suporte da comunidade
+ Tradutor (Polonês)
+ Tradutor (Checo)
+ Tradutor (Espanhol)
+
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 35fadc1244..4d55cb73e7 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -7,7 +7,6 @@
ВключитьОткрыть¯\\_(ツ)_/¯\n\n Здесь ничего!
- Запишите событие!Добавьте действие!¯\\_(ツ)_/¯\n\nСоздайте мэппинг!¯\\_(ツ)_/¯\n\nВы не выбрали никаких действий для этого ярлыка!
@@ -161,10 +160,6 @@
ДействияСобытияОтпечаток пальцев
-
- События нажатий
- Отпечаток пальца
-
@@ -1035,4 +1030,6 @@
Переводчик (Чешский)Переводчик (Испанский)
+
+
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index cb2f69c12e..083daab72b 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -5,7 +5,6 @@
ZapnúťOtvoriť¯\\_(ツ)_/¯\n\nNič tu nie je!
- Nahrať spúšť!Pridať akciu!̄\\_(ツ)_/ ̄\n\nVytvoriť mapu klávesov!̄\\_(ツ)_/ ̄\n\nNevybrali ste pre tento odkaz žiadne akcie!
@@ -154,10 +153,6 @@
AkcieSpúšťačOdtlačok prsta
-
- @string/tab_keyevents
- @string/tab_fingerprint
-
@@ -871,4 +866,6 @@
DiscordLicencia
+
+
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 8b2b1737b0..aef0a460ed 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -63,4 +63,6 @@
+
+
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..aef0a460ed
--- /dev/null
+++ b/app/src/main/res/values-uk/strings.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..bc2e1744cb
--- /dev/null
+++ b/app/src/main/res/values-vi/strings.xml
@@ -0,0 +1,1052 @@
+
+
+ Giải phóng chìa khóa của bạn!
+ Không có hành động nào được chọn
+ Key Mapper yêu cầu sử dụng dịch vụ trợ năng để dịch vụ này có thể phát hiện và thay đổi thao tác nhấn nút của bạn khi bạn ở ngoài ứng dụng. Bản đồ chính của bạn sẽ chỉ hoạt động khi bạn đã bật dịch vụ trợ năng. Nó cũng phải được bật để tạo trình kích hoạt.
+ %d Đã chọn
+ Cho phép
+ Mở
+ ¯\\_(ツ)_/¯\n\nKhông có gì ở đây!
+ Thêm hành động!
+ ¯\\_(ツ)_/¯\n\n Tạo một hành động!
+ ¯\\_(ツ)_/¯\n\nBạn chưa chọn bất kỳ hành động nào cho phím tắt này!
+ Bạn chưa tạo bất kỳ bản đồ quan trọng nào!
+ Mọi thứ đều có vẻ tốt!
+ Ứng dụng sẽ hoạt động nhưng có thể cần khắc phục một số lỗi tùy thuộc vào việc bạn đang làm.
+ Thiết bị của bạn không hỗ trợ một số hành động.
+ Hành động không được hỗ trợ
+ Yêu cầu root
+ Nhấn…
+ Không có hành động nào
+ Không có hành động kích hoạt
+ Mã khóa không xác định: %s
+ Tên thiết bị không xác định
+ Bật
+ Tắt
+ Theo hệ thống
+ Thiết bị này
+ Bất kỳ thiết bị nào
+ Không biết tên của thiết bị này
+ Mặc định
+ Bật dịch vụ trợ năng
+ Khởi động lại dịch vụ trợ năng
+ Chia sẻ
+ Dừng lặp lại khi…
+ Trình kích hoạt được phát hiện
+ Phím được nhấn lại
+ Đã đạt đến giới hạn
+ Vuốt lại
+ Trình kích hoạt được phát hiện
+ Phím được nhấn lại
+ Hiển thị ứng dụng ẩn
+ Sửa đổi
+ Chuyển đổi
+ QUAN TRỌNG!!! Các tọa độ này chỉ chính xác khi màn hình của bạn cùng hướng với ảnh chụp màn hình! Hành động này sẽ hủy mọi thao tác chạm hoặc cử chỉ bạn đang thực hiện trên màn hình.\n\nNếu bạn cần trợ giúp tìm tọa độ của một điểm trên màn hình, hãy chụp ảnh màn hình rồi nhấn vào ảnh chụp màn hình nơi bạn muốn nhấn vào hành động này.
+ Lưu ý: Khi sử dụng \"pinch in\" X và Y là tọa độ END, khi sử dụng \"pinch out\" X và Y là tọa độ START.
+ Không biết tên máy!
+ Nhấn vào hành động để khắc phục!
+ Nhấn vào các hạn chế để khắc phục!
+ Thực hiện hành động
+ Giữ cho đến khi kích hoạt…
+ Không có thiết bị
+ ¯\\_(ツ)_/¯\n\nKhông có tính năng bổ sung!
+ Tạo sơ đồ phím mới
+ Đã hoàn tất việc định cấu hình hành động sự kiện quan trọng
+ Chọn tọa độ xong
+ Nhật ký hành động
+ Gửi tới
+ Có gì mới
+ Bấm vào phím trên thiết bị mà bạn muốn nhập. \n\nQUAN Trọng! Việc nhập phím này dưới dạng hành động sẽ chỉ hoạt động nếu bạn đang sử dụng bàn phím tương thích với Key Mapper.
+ QUAN TRỌNG! Việc nhập mã khóa này dưới dạng hành động sẽ chỉ có tác dụng nếu bạn đang sử dụng bàn phím tương thích với Key Mapper.
+ Tệp âm thanh sẽ được sao chép vào thư mục dữ liệu riêng tư của Key Mapper, điều đó có nghĩa là các thao tác của bạn vẫn hoạt động ngay cả khi tệp bị di chuyển hoặc xóa. Nó cũng sẽ được sao lưu cùng với bản đồ chính của bạn trong thư mục zip. Bạn có thể xóa các tập tin âm thanh đã lưu trong cài đặt.
+ Nhập một số văn bản bạn muốn chèn vào khi thực hiện hành động này.
+ Nhập số điện thoại.
+ Nhập URL mà bạn muốn mở. http://, https:// hoặc www. không bắt buộc.
+ Không thể tìm thấy bất kỳ thiết bị được ghép nối nào. Bluetooth đã được bật chưa?
+ Tùy chọn \"Cho phép các ứng dụng khác kích hoạt sơ đồ bàn phím này\" sẽ được bật cho sơ đồ bàn phím mà bạn chọn nếu chưa bật. Nếu sau này bạn tắt tùy chọn này thì mọi phím tắt hoặc Ý định kích hoạt sơ đồ phím này sẽ không hoạt động.
+ Đã bật
+ Đã tắt
+ Khôi phục
+ Sau khi đã bật quản trị viên thiết bị, bạn phải TẮT KÍCH HOẠT nó nếu muốn gỡ cài đặt Key Mapper.
+ Thêm một hạn chế!
+ Đợi %sms
+ Bắt đầu hoạt động: %s
+ Bắt đầu dịch vụ: %s
+ Gửi phát sóng: %s
+ Sơ đồ chính UUID
+ Sử dụng shell (chỉ ROOT)
+ Key Mapper yêu cầu quyền sửa đổi chế độ Không làm phiền nếu bạn muốn các nút hoạt động như mong đợi ở chế độ Không làm phiền!
+ Trình kích hoạt này sẽ không hoạt động như mong đợi ở chế độ Không làm phiền!
+ Tùy chọn kích hoạt khi màn hình tắt cần có quyền root để hoạt động!
+ Tùy chọn kích hoạt khi màn hình tắt sẽ không hoạt động!
+ Trình kích hoạt này sẽ không hoạt động khi đang đổ chuông hoặc đang gọi điện thoại!
+ Android không cho phép các dịch vụ trợ năng phát hiện các lần nhấn nút âm lượng khi điện thoại của bạn đang đổ chuông hoặc đang trong cuộc gọi điện thoại, nhưng nó cho phép các dịch vụ phương thức nhập liệu phát hiện chúng. Do đó, bạn phải sử dụng một trong các bàn phím Key Mapper nếu muốn trình kích hoạt này hoạt động.
+ Quá nhiều ngón tay để thực hiện cử chỉ do giới hạn của Android.
+ Thời lượng cử chỉ quá cao do giới hạn của Android.
+ Hành động của bạn sẽ ngừng hoạt động ngẫu nhiên!
+ Hành động của bạn đã bị tạm dừng!
+ Bỏ tạm dừng
+ Dịch vụ trợ năng cần được bật để hành động của bạn hoạt động!
+ Điện thoại của bạn đã tắt Key Mapper khi nó ở chế độ nền hoặc bị hỏng!
+ Dịch vụ trợ năng đã được kích hoạt! Hành động của bạn sẽ hoạt động.
+ Ghi nhật ký bổ sung được bật! Hãy tắt tính năng này nếu bạn không cố gắng khắc phục sự cố.
+ Tắt
+ Thông tin
+
+ Mở %s
+ Nhấn phím \'%s\'
+ Nhập \'%s\'
+ Nhập %s%s
+ Nhập %s qua shell
+ Nhập %s%s từ %s
+ Mở %s
+ Nhấn vào tọa độ %d, %d
+ Nhấn vào tọa độ %d, %d (%s)
+ Vuốt bằng %d ngón tay từ tọa độ %d/%d đến %d/%d trong %dms
+ Vuốt bằng %d ngón tay từ tọa độ %d/%d đến %d/%d trong %dms (%s)
+ %s bằng %d ngón tay(s) trên tọa độ %d/%d với khoảng cách chụm là %dpx tính bằng %dms
+ %s với %d ngón tay trên tọa độ %d/%d đến với khoảng cách chụm là %dpx %dms (%s)
+ Gọi %s
+ Phát âm thanh: %s
+
+
+ Tùy chọn
+ Hành động
+ Kích hoạt
+ Hạn chế
+ Vuốt lên
+ Vuốt xuống
+ Vuốt sang trái
+ Vuốt sang phải
+ Kiểu nhấp chuột
+ Tiện ích bổ sung
+
+
+ Bắt đầu X
+ Bắt đầu Y
+ Kết thúc X
+ Kết thúc Y
+ Khoảng cách chụm (px)
+ Kiểu kẹp
+ Chụm vào
+ Chụm ra
+ Mã khóa
+ Từ thiết bị
+ Tên phím tắt
+ Mô tả tọa độ (tùy chọn)
+ Văn bản cần nhập
+ Url để mở
+ Số điện thoại để gọi
+ Hoạt động
+ Thể loại
+ Dữ liệu
+ Bưu kiện
+ Class
+ Tên
+ Giá trị (%s)
+ Mô tả cho Key Mapper (bắt buộc)
+ Flags
+ Mô tả tập tin âm thanh
+ SSID mạng Wi-Fi
+
+
+ Đồng thời
+ Theo thứ tự
+ VÀ
+ HOẶC
+ Nhấn
+ Nhấn giữ
+ Nhấn đúp
+ Đúng
+ Sai
+ Hoạt động
+ Dịch vụ
+ Máy thu phát sóng
+
+
+ Kích hoạt và hành động
+ Những hạn chế và hơn thế nữa
+ Tùy chọn
+ Hạn chế
+ Hành động
+ Kích hoạt
+ Dấu vân tay
+
+
+
+
+ Đã chọn %s
+ Sao lưu thành công!
+ Sao lưu thất bại!
+ Khôi phục thành công!
+ Khôi phục thất bại!
+ Tự động sao lưu thành công!
+ Tự động sao lưu thất bại!
+ Đã chụp ảnh màn hình
+ IO ngoại lệ ¯\\_(ツ)_/¯
+ Độ phân giải ảnh chụp màn hình không khớp với độ phân giải của thiết bị này!
+ Đã sao chép sơ đồ khóa UUID vào bảng nhớ tạm
+ Bạn đã kích hoạt hành động
+ Nhật ký được sao chép
+ Tính năng root hiện đã được kích hoạt
+ Chọn tập tin âm thanh không thành công! Kiểm tra nhật ký.
+ Bạn chưa lưu tập tin âm thanh nào!
+ Key Mapper đã sử dụng Shizuku để tự cấp quyền WRITE_SECURE_SETTINGS
+ Key Mapper đã sử dụng Root để tự cấp quyền WRITE_SECURE_SETTINGS
+
+
+ Thời gian chờ kích hoạt phím tiếp theo (ms)
+ Độ trễ nhấn giữ (ms)
+ Thời gian chờ nhấn đúp (ms)
+ Trì hoãn cho đến khi lặp lại (ms)
+ Giới hạn lặp lại
+ Lặp lại mỗi… (ms)
+ Thời gian rung (ms)
+ Số lần lặp lại
+ Mỗi lần lặp lại bao nhiêu lần
+ Độ trễ trước hành động tiếp theo (ms)
+ Thời gian giữ phím (ms)
+ Thời lượng vuốt (ms)
+ Số ngón tay
+ Tọa độ để thiết lập với ảnh chụp màn hình
+ Bắt đầu
+ Kết thúc
+ Thời lượng chụm (ms)
+ Số ngón tay
+
+
+ %s ở phía trước
+ %s không ở phía trước
+ %s đang phát phương tiện
+ %s không phát phương tiện
+ %s đã được kết nối
+ %s bị ngắt kết nối
+ Màn hình bật
+ Màn hình tắt
+ đèn pin %s đang tắt
+ Đèn pin %s đang bật
+ và
+ Hoặc
+ Ứng dụng
+ Bluetooth
+ Màn hình
+ Định hướng
+ Ứng dụng ở phía trước
+ Ứng dụng không ở nền trước
+ Thiết bị Bluetooth đã được kết nối
+ Thiết bị Bluetooth bị ngắt kết nối
+ Màn hình bật
+ Màn hình tắt
+ Màn hình xoay (0°)
+ Màn hình xoay (90°)
+ Màn hình xoay (180°)
+ Màn hình xoay (270°)
+ Màn hình dọc (bất kỳ)
+ Màn hình ngang (bất kỳ)
+ Ứng dụng phát âm thanh
+ Ứng dụng không phát âm thanh
+ Âm thanh đang phát
+ Không có âm thanh nào đang phát
+ Đèn pin đang bật
+ Đèn pin đã tắt
+ WiFi đang bật
+ Wi-Fi đã tắt
+ Đã kết nối với mạng WiFi
+ Đã ngắt kết nối khỏi mạng WiFi
+ Bạn sẽ phải nhập SSID theo cách thủ công vì các ứng dụng không được phép truy vấn danh sách các mạng WiFi đã biết trên Android 10 trở lên. Để trống nếu có bất kỳ mạng WiFi nào phù hợp.
+ Bất kì
+ Đã kết nối với WiFi %s
+ Đã ngắt kết nối với WiFi %s
+ Đã kết nối với bất kỳ WiFi nào
+ Đã ngắt kết nối và không có WiFi
+ Phương thức nhập được chọn
+ %s được chọn
+ Phương thức nhập không được chọn
+ %s không được chọn
+ Thiết bị đã bị khóa
+ Thiết bị đã được mở khóa
+ Trong cuộc gọi điện thoại
+ Không có trong cuộc gọi điện thoại
+ Điện thoại đổ chuông
+ Sạc
+ Xả
+ Màn hình xoay (0°)
+ Màn hình xoay (90°)
+ Màn hình xoay (180°)
+ Màn hình xoay (270°)
+
+
+ Nhấn
+ Nhấn giữ
+ Nhấn đúp
+
+
+
+
+ Chọn hành động
+
+
+ Tắt tính năng diệt ứng dụng
+ Hãy làm theo hướng dẫn tại Dontkillmyapp.com để chỉ cho bạn cách tắt tất cả các \"tính năng\" tiêu diệt ứng dụng trên điện thoại của bạn. \n\nSau khi đọc hướng dẫn, bạn sẽ cần chuyển sang trang trình bày tiếp theo và khởi động lại dịch vụ trợ năng.
+ Mở hướng dẫn
+ Khởi động lại dịch vụ trợ năng
+ Dịch vụ trợ năng phải được khởi động lại. Tắt nó đi và bật lại.
+ Khởi động lại
+ Báo cáo lỗi
+ Chọn vị trí để lưu báo cáo lỗi bằng cách nhấn vào \"tạo báo cáo\". Trang trình bày tiếp theo sẽ cho biết cách bạn có thể gửi nó cho nhà phát triển.
+ Tạo báo cáo
+ Chia sẻ báo cáo
+ Có 2 cách để chia sẻ báo cáo lỗi cho nhà phát triển. Tham gia máy chủ Discord hoặc tạo sự cố GitHub. Đảm bảo rằng bạn đính kèm báo cáo lỗi vào tin nhắn của mình!
+ Discord
+ GitHub
+
+
+ Cài đặt
+ Xong
+ Chọn tất cả
+ Thông tin
+ Tìm kiếm
+ Hướng dẫn bắt đầu nhanh
+ Help
+ Cho phép
+ Vô hiệu hóa
+ Vô hiệu hóa tất cả
+ Kích hoạt tất cả
+ Báo cáo lỗi
+ Hiển thị hộp chọn phương thức nhập
+ Cơ sở dữ liệu hạt giống
+ Lưu
+ Nhân bản
+ Lưu
+ Khôi phục
+ Sao lưu mọi thứ
+ Sao lưu tất cả
+ Nhấn để tạm dừng
+ Nhấn để tiếp tục
+ Khôi phục
+ Lưu
+ Chuyển đổi tin nhắn ngắn
+ Sao chép
+ Xóa
+
+
+ Thêm hành động
+ Ghi kích hoạt
+ Xong
+ Lưu
+ Sửa chữa
+ %d…
+ Thêm hạm chế
+ Chọn mã khóa
+ Đúng!
+ Chọn hành động
+ Thêm bổ sung
+ Tạo lối tắt trình khởi chạy
+ Tạo lối tắt thủ công
+ Hướng dẫn
+ Help
+ Chọn ảnh chụp màn hình (tùy chọn)
+ Chọn hoạt động
+ Set flags
+ Sao chép
+ Không giới hạn
+ Chọn tập tin âm thanh
+ Chỉnh sửa hành động
+ Thay thế hành động
+
+
+ Bạn có chắc không?
+ Tạo tiêu đề lối tắt
+ Cần có quyền root!
+ Hơn
+ Chọn luồng
+ Chọn đèn nháy
+ Không thể tìm thấy trang cài đặt trợ năng
+ Không thể ghi kích hoạt?
+ Hủy thay đổi của bạn
+ Bạn có chắc chắn muốn hủy các thay đổi của mình không?
+ Nếu bạn biết điện thoại của mình chưa được root hoặc bạn không biết root là gì thì bạn không thể sử dụng các tính năng chỉ hoạt động trên các thiết bị đã root. Khi bạn nhấn \'OK\', bạn sẽ được đưa đến cài đặt. Trong cài đặt, cuộn xuống cuối và nhấn \'Key Mapper có quyền root\' để bạn có thể sử dụng các tính năng/hành động gốc.
+ Đang tải xuống…
+ Nhấn lâu chỉ có tác dụng với các nút âm lượng và điều hướng vật lý. Nếu bạn bật tính năng này cho các phím khác, các phím đó sẽ không hoạt động khi chúng không được nhấn lâu.
+ Cấp quyền WRITE_SECURE_SETTINGS
+ Cần có PC/Mac để cấp quyền này. Đọc hướng dẫn trực tuyến.
+ Thiết bị của bạn dường như không có trang cài đặt dịch vụ trợ năng. Nhấn vào \"hướng dẫn\" để đọc hướng dẫn trực tuyến giải thích cách khắc phục vấn đề này.
+ "Nhiều phím không thể được nhấn đúp cùng một lúc."
+ Các phím cần được liệt kê từ trên xuống dưới theo thứ tự chúng sẽ được giữ.
+ Trình kích hoạt \"trình tự\" có thời gian chờ không giống như trình kích hoạt song song. Điều này có nghĩa là sau khi nhấn phím đầu tiên, bạn sẽ có một khoảng thời gian nhất định để nhập các phím còn lại vào bộ kích hoạt. Tất cả các phím mà bạn đã thêm vào trình kích hoạt sẽ không thực hiện hành động thông thường cho đến khi hết thời gian chờ. Bạn có thể thay đổi thời gian chờ này trong tab \"Tùy chọn\".
+ Android không cho phép ứng dụng nhận danh sách các thiết bị Bluetooth được kết nối (không ghép nối). Ứng dụng chỉ có thể phát hiện khi chúng được kết nối và ngắt kết nối. Vì vậy, nếu thiết bị Bluetooth của bạn đã được kết nối với thiết bị của bạn khi dịch vụ trợ năng khởi động, bạn sẽ phải kết nối lại thiết bị đó để ứng dụng biết thiết bị đó đã được kết nối.
+ Thay đổi vị trí sao lưu hoặc tắt tự động sao lưu?
+ Các ràng buộc bật/tắt màn hình sẽ chỉ hoạt động nếu bạn đã bật tùy chọn sơ đồ phím \"phát hiện trình kích hoạt khi màn hình tắt\". Tùy chọn này sẽ chỉ hiển thị đối với một số phím (ví dụ: nút âm lượng) và nếu bạn đã root. Xem danh sách các phím được hỗ trợ trên trang Trợ giúp.
+ Nếu bạn đã chọn bất kỳ khóa màn hình nào khác, chẳng hạn như mã PIN hoặc Mẫu thì bạn không phải lo lắng. Nhưng nếu bạn có khóa màn hình Mật khẩu, bạn sẽ *KHÔNG* có thể mở khóa điện thoại của mình nếu bạn sử dụng Phương thức nhập cơ bản của Key Mapper vì nó không có GUI. Bạn có thể cấp quyền cho Key Mapper WRITE_SECURE_SETTINGS để nó có thể hiển thị thông báo chuyển sang và từ bàn phím. Có hướng dẫn về cách thực hiện việc này nếu bạn nhấn vào dấu chấm hỏi ở cuối màn hình.
+ Chọn phương thức nhập cho các hành động yêu cầu. Bạn có thể thay đổi điều này sau bằng cách nhấn vào \"Chọn bàn phím để thực hiện thao tác\" ở menu dưới cùng của màn hình chính.
+ Bạn cần chọn bố cục bàn phím \"Caps Lock to camera\" cho bàn phím của mình nếu không phím Caps Lock vẫn sẽ khóa mũ. Bạn có thể tìm thấy cài đặt này trong cài đặt thiết bị của mình -> Ngôn ngữ và Phương thức nhập -> Bàn phím vật lý -> Nhấn vào bàn phím của bạn -> Thiết lập bố cục bàn phím. Thao tác này sẽ ánh xạ lại phím Caps Lock thành KEYCODE_CAMERA để Key Mapper có thể ánh xạ lại phím đó đúng cách.\n\nSau khi thực hiện xong việc này, bạn phải xóa phím kích hoạt Caps Lock và ghi lại phím Caps Lock. Nó sẽ hiện \"Camera\" thay vì \"Caps Lock\" nếu bạn thực hiện đúng các bước.
+ Khởi động lại thiết bị của bạn nếu nút \"Kích hoạt ghi\" đang đếm ngược và các nút bạn đang nhấn không hiển thị. Nếu các nút của bạn vẫn không hiển thị sau khi khởi động lại thì Key Mapper không hỗ trợ các nút của bạn. Không có cách khắc phục cho việc này.
+ Không có thiết bị bên ngoài nào được kết nối.
+ Cài đặt bàn phím GUI Key Mapper
+ Điều này rất được khuyến khích! Đây là bàn phím thích hợp mà bạn có thể sử dụng với Key Mapper. Công cụ tích hợp sẵn trong Key Mapper (Phương thức nhập liệu cơ bản) không có bàn phím trên màn hình. Chọn nơi bạn muốn cài đặt nó.
+ Cài đặt Bàn phím Leanback của Key Mapper
+ Điều này rất được khuyến khích! Đây là bàn phím thích hợp cho Android TV mà bạn có thể sử dụng với Key Mapper. Công cụ tích hợp sẵn trong Key Mapper (Phương thức nhập liệu cơ bản) không có bàn phím trên màn hình. Chọn nơi bạn muốn cài đặt nó.
+ Cài đặt bàn phím GUI Key Mapper
+ Chọn nơi bạn muốn tải xuống từ đó.
+ Cài đặt Bàn phím Leanback của Key Mapper
+ Chọn nơi bạn muốn tải xuống từ đó.
+ Hành động này cần thiết lập thêm
+ Có 3 cách để bạn có thể thiết lập thiết bị của mình sử dụng tác vụ này. Dưới đây là những ưu điểm và nhược điểm của mỗi. \n\n1. Tải xuống Shizuku (được khuyến nghị). Bạn không cần phải sử dụng bàn phím ảo khác với bàn phím bạn đang sử dụng nhưng sẽ cần một phút thiết lập mỗi khi bạn khởi động lại thiết bị của mình. \n\n2. Tải xuống Bàn phím GUI Key Mapper. Đây là bàn phím ảo mà bạn có thể sử dụng với Key Mapper nhưng bạn sẽ không thể sử dụng bàn phím mà bạn hiện đang sử dụng, chẳng hạn như Gboard. \n\n3. Không làm gì cả và sử dụng bàn phím Key Mapper tích hợp sẵn. Điều này không được khuyến khích vì bạn sẽ không bàn phím ảo khi sử dụng Key Mapper! Không có lợi thế.
+ Hành động này cần thiết lập thêm
+ Có 3 cách để bạn có thể thiết lập thiết bị của mình sử dụng tác vụ này. Dưới đây là những ưu điểm và nhược điểm của mỗi. \n\n1. Tải xuống Shizuku (được khuyến nghị). Bạn không cần phải sử dụng bàn phím ảo khác với bàn phím bạn đang sử dụng nhưng sẽ cần một phút thiết lập mỗi khi bạn khởi động lại thiết bị của mình. \n\n2. Tải xuống Bàn phím Leanback Key Mapper. Đây là bàn phím ảo được tối ưu hóa cho Android TV mà bạn có thể sử dụng với Key Mapper nhưng bạn sẽ không thể sử dụng bàn phím mà bạn hiện đang sử dụng, chẳng hạn như Gboard. \n\n3. Không làm gì cả và sử dụng bàn phím Key Mapper tích hợp sẵn. Điều này không được khuyến khích vì bạn sẽ không bàn phím ảo khi sử dụng Key Mapper! Không có lợi thế.
+ Vô hiệu hóa tối ưu hóa pin
+ Bạn PHẢI đọc tất cả này nếu không bạn sẽ thất vọng trong tương lai!\n\nNhấn vào \"sửa một phần\" có thể ngăn Android dừng ứng dụng khi ứng dụng đang chạy trong nền.\n\nĐiều này KHÔNG ĐỦ. Giao diện OEM của bạn như MIUI hoặc Samsung Experience có thể có các tính năng tiêu diệt ứng dụng khác, vì vậy bạn PHẢI tắt chúng cho Key Mapper bằng cách làm theo hướng dẫn trực tuyến tại Dontkillmyapp.com.
+ Gửi phản hồi
+ Vui lòng đọc hướng dẫn về cách báo cáo sự cố trên trang web.
+ Bật dịch vụ trợ năng để bạn có thể ghi lại trình kích hoạt.
+ Khởi động lại dịch vụ trợ năng bằng cách tắt và bật để bạn có thể ghi lại trình kích hoạt.
+ Bật dịch vụ trợ năng để bạn có thể kiểm tra hành động.
+ Khởi động lại dịch vụ trợ năng bằng cách tắt và bật để bạn có thể kiểm tra hành động.
+ Khởi động lại dịch vụ trợ năng bằng cách tắt và bật.
+ Việc sử dụng trình kích hoạt này có thể gây ra màn hình đen khi bạn mở khóa thiết bị sau khi sử dụng cài đặt ghim màn hình trong cài đặt của thiết bị. Điều này có thể được khắc phục bằng cách khởi động lại. Điều này không xảy ra trên tất cả các thiết bị vì vậy hãy cẩn thận và tắt cài đặt nếu xảy ra!
+ Đặt lại bản đồ cử chỉ vân tay
+ Bạn có chắc chắn muốn đặt lại bản đồ cử chỉ vân tay của mình không?
+ Key Mapper đã bị hỏng
+ Rất có thể điện thoại của bạn đã tắt Key Mapper khi nó đang cố chạy ở chế độ nền. Đây không là lỗi của nhà phát triển và họ không thể làm gì để khắc phục nên vui lòng đừng để lại đánh giá xấu 😃.
+
+ \n\nTrước đây, bạn đã làm theo hướng dẫn trên Dontkillmyapp.com để ngăn điện thoại của mình tắt Key Mapper chưa?
+ Đúng
+ Không
+ Không tạo được báo cáo lỗi
+ Yêu cầu sự cho phép của Shizuku
+ Vì bạn đang sử dụng Shizuku nên bạn nên cấp quyền này vì một số tính năng trong Key Mapper có thể được thực hiện mà bạn không cần phải định cấu hình bất cứ điều gì (Ví dụ: nhập mã khóa mà không cần sử dụng bàn phím Key Mapper).
+ Sửa lỗi
+ Cần có sự cho phép
+ Key Mapper cần có quyền \"thiết bị lân cận\" để có thể lấy danh sách các thiết bị Bluetooth được ghép nối.
+ Bạn chưa cài đặt ứng dụng tệp nào cho phép bạn tạo tệp cho Key Mapper. Vui lòng cài đặt trình quản lý tập tin.
+ Bạn chưa cài đặt ứng dụng tệp nào cho phép bạn chọn tệp cho Key Mapper. Vui lòng cài đặt trình quản lý tập tin.
+ Dịch vụ trợ năng phải được kích hoạt
+ @string/accessibility_service_explanation
+ Cấp quyền truy cập Không làm phiền
+ Bạn sẽ được đưa đến trang cài đặt của thiết bị để quản lý những ứng dụng nào có thể sửa đổi trạng thái Không làm phiền. Tính năng này không xuất hiện trên một số thiết bị vì vậy hãy nhấn vào không hiển thị lại nếu bạn không thấy Key Mapper trong danh sách.
+ Đúng
+ Xác nhận
+ Xong
+ Chọn tham gia
+ Hướng dẫn
+ Hướng dẫn
+ Hướng dẫn
+ Kích hoạt tính năng root
+ Cấp quyền
+ Tham gia
+ Thay đổi
+ Sửa một phần
+ Được rồi
+ Bật
+ Khởi động lại
+ Không bao giờ hiển thị lại
+ Mở hướng dẫn trực tuyến
+ Tắt
+ Tránh xa
+ Không
+ Hủy bỏ
+ Không hiển thị lại
+ Hướng dẫn trực tuyến
+ Cài đặt
+ Tài liệu
+ Nhật ký thay đổi
+ Báo cáo lỗi
+ Khởi động lại
+ Đi đến hướng dẫn
+ Shizuku
+ Bàn phím GUI của Key Mapper
+ Bàn phím Leanback của Key Mapper
+ Không làm gì cả
+ Sửa chữa
+ Hủy bỏ
+ Sửa chữa
+
+
+ Bộ chọn bàn phím
+ Tạm dừng/Tiếp tục ánh xạ
+ Cảnh báo bàn phím bị ẩn
+ Chuyển đổi bàn phím Key Mapper
+ Tính năng mới
+ Nhấn để thay đổi bàn phím của bạn.
+ Hộp chọn bàn phím
+ Đang chạy
+ Nhấn để mở Key Mapper.
+ Tạm dừng
+ Đã tạm dừng
+ Nhấn để mở Key Mapper.
+ Khởi chạy
+ Loại bỏ
+ Khởi động lại
+ Dịch vụ trợ năng bị vô hiệu hóa
+ Nhấn để bắt đầu dịch vụ trợ năng.
+ Dịch vụ trợ năng cần khởi động lại!
+ Dịch vụ trợ năng đã bị hỏng! Điện thoại của bạn có thể đang tích cực tắt nó! Nhấn để khởi động lại dịch vụ trợ năng.
+
+ Dừng dịch vụ
+ Bàn phím bị ẩn!
+ Nhấn \'hiển thị bàn phím\' để bắt đầu hiển thị lại bàn phím.
+ Chuyển đổi bàn phím Key Mapper
+ Nhấn \'chuyển đổi\' để chuyển sang và từ bàn phím Key Mapper.
+ Chuyển đổi
+ Ánh xạ lại cử chỉ vân tay với Key Mapper!
+ Thiết bị của bạn hỗ trợ ánh xạ lại các thao tác vuốt trên cảm biến vân tay. Nhấn để bắt đầu ánh xạ lại!
+ Bạn cần thiết lập lại một số cài đặt!
+ Có vẻ như bạn đang sử dụng cài đặt này để tự động thay đổi bàn phím hoặc hiển thị bộ chọn phương thức nhập khi thiết bị Bluetooth kết nối và ngắt kết nối. Key Mapper hiện cho phép bạn sử dụng bất kỳ thiết bị đầu vào nào chứ không chỉ các thiết bị Bluetooth. Không có cách nào để di chuyển cài đặt cũ theo cách chức năng mới sẽ hoạt động, do đó bạn sẽ phải chọn lại thiết bị trong cài đặt Key Mapper. Nhấn để mở Key Mapper.
+
+
+ Độ trễ nhấn giữ mặc định (ms)
+ Cần nhấn một nút trong bao lâu để được phát hiện là nhấn lâu. Mặc định là 500ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính.
+ Thời lượng nhấn đúp mặc định (ms)
+ Một nút phải được nhấn đúp nhanh đến mức nào để được phát hiện là nhấn đúp. Mặc định là 300ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính.
+ Rung trong bao lâu nếu chế độ rung được bật cho sơ đồ phím. Mặc định là 200ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính.
+ Thời lượng rung mặc định (ms)
+ Cần giữ nút kích hoạt trong bao lâu để hành động bắt đầu lặp lại. Mặc định là 400ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính.
+ Độ trễ mặc định cho đến khi lặp lại (ms)
+ Độ trễ giữa mỗi lần một hành động được lặp lại. Mặc định là 50ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính.
+ Độ trễ mặc định giữa các lần lặp lại (ms)
+ Thời gian cho phép để hoàn thành một trình kích hoạt trình tự. Mặc định là 1000ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính.
+ Thời gian chờ kích hoạt trình tự mặc định (ms)
+ Khôi phục
+ Buộc tất cả các bản đồ chính rung.
+ Buộc rung
+ Thông báo bộ chọn bàn phím
+ Hiển thị thông báo liên tục để cho phép bạn chọn bàn phím.
+ Tạm dừng/tiếp tục thông báo hành động
+ Hiển thị thông báo liên tục bắt đầu/tạm dừng hành động của bạn.
+ Tự động sao lưu cài đặt tới một vị trí được chỉ định
+ Không có địa điểm nào được chọn.
+ Chọn thiết bị
+ Tự động hiển thị hộp chọn bàn phím
+ Khi thiết bị bạn đã chọn kết nối hoặc ngắt kết nối, bộ chọn bàn phím sẽ tự động hiển thị. Chọn các thiết bị dưới đây.
+ Tự động thay đổi bàn phím ảo khi một thiết bị (ví dụ: bàn phím) kết nối/ngắt kết nối
+ Bàn phím Key Mapper được sử dụng lần cuối sẽ được chọn tự động khi thiết bị được chọn được kết nối. Bàn phím thông thường của bạn sẽ được tự động chọn khi thiết bị ngắt kết nối.
+ Tự động thay đổi bàn phím ảo khi bạn bắt đầu nhập văn bản
+ Bàn phím không phải Key Mapper được sử dụng lần cuối sẽ tự động được chọn khi bạn cố mở bàn phím. Bàn phím Key Mapper của bạn sẽ được chọn tự động sau khi bạn ngừng sử dụng bàn phím.
+ Hiển thị thông báo trên màn hình khi tự động thay đổi bàn phím
+ Key Mapper có quyền root
+ Bật tính năng này nếu bạn muốn sử dụng các tính năng/hành động chỉ hoạt động trên các thiết bị đã root. Key Mapper phải có quyền root từ ứng dụng quản lý quyền truy cập root của bạn (ví dụ: Magisk, SuperSU) để các tính năng này hoạt động. Chỉ bật tính năng này nếu bạn biết thiết bị của mình đã được root và bạn đã cấp quyền root cho Key Mapper.
+ Chủ đề tối
+ Cài đặt thông báo
+ Chuyển đổi giữa bàn phím Key Mapper và bàn phím mặc định của bạn khi bạn chạm vào thông báo.
+ Chuyển đổi thông báo bàn phím Key Mapper
+ Tự động thay đổi bàn phím khi chuyển đổi sơ đồ phím
+ Tự động chọn bàn phím Key Mapper khi bạn tiếp tục lại sơ đồ chính và chọn bàn phím mặc định khi tạm dừng chúng.
+ Ẩn cảnh báo màn hình chính
+ Ẩn cảnh báo ở đầu màn hình chính.
+ Hiển thị 5 ký tự đầu tiên của id thiết bị cho trình kích hoạt cụ thể của thiết bị
+ Điều này rất hữu ích để phân biệt giữa các thiết bị có cùng tên.
+ Sửa bàn phím được đặt thành tiếng Anh Mỹ
+ Điều này khắc phục những bàn phím không có bố cục bàn phím chính xác khi bật dịch vụ trợ năng. Nhấn để đọc thêm và định cấu hình.
+ Sửa bàn phím được đặt thành tiếng Anh Mỹ
+ Có một lỗi trong Android 11 là việc bật dịch vụ trợ năng khiến Android nghĩ rằng tất cả các thiết bị bên ngoài đều là cùng một thiết bị ảo bên trong. Vì nó không thể xác định chính xác các thiết bị này nên nó không biết nên sử dụng bố cục bàn phím nào với chúng nên nó mặc định là tiếng Anh Mỹ ngay cả khi đó là bàn phím tiếng Đức chẳng hạn. Bạn có thể sử dụng Key Mapper để khắc phục sự cố này bằng cách thực hiện theo các bước bên dưới.
+ 4. Chọn thiết bị
+ 1. Cài đặt Bàn phím GUI Key Mapper (tùy chọn)
+ 1. Cài đặt Bàn phím Leanback của Key Mapper (tùy chọn)
+ 2. Kích hoạt Bàn phím GUI Key Mapper hoặc Phương thức nhập cơ bản của Key Mapper
+ 2. Kích hoạt Bàn phím Leanback của Key Mapper hoặc Phương thức nhập cơ bản của Key Mapper
+ 3. Sử dụng bàn phím bạn vừa kích hoạt
+ (Được khuyến nghị) Đọc hướng dẫn sử dụng cho cài đặt này.
+ Cho phép ghi nhật ký bổ sung
+ Xem và chia sẻ nhật ký
+ Báo cáo vấn đề
+ Xóa tập tin âm thanh
+ Xóa các tệp âm thanh có thể được sử dụng cho tác vụ Âm thanh.
+ Cấp quyền
+ Đã cấp quyền
+ 1. Shizuku chưa được cài đặt! Nhấn để tải xuống ứng dụng Shizuku.
+ 1. Shizuku đã được cài đặt.
+ 2. Shizuku chưa bắt đầu! Nhấn để mở ứng dụng Shizuku rồi đọc hướng dẫn giải thích cách khởi động ứng dụng.
+ 2. Shizuku bắt đầu.
+ 3. Key Mapper không được phép sử dụng Shizuku. Nhấn để cấp quyền này.
+ 3. Key Mapper sẽ tự động sử dụng Shizuku. Nhấn để đọc tính năng Key Mapper nào sử dụng Shizuku.
+ Tùy chọn cài đặt mặc định
+ Thay đổi các tùy chọn mặc định cho ánh xạ của bạn.
+
+
+ Tự động hiển thị hộp chọn bàn phím
+ Nhấn để xem cài đặt cho phép bạn tự động hiển thị bộ chọn bàn phím.
+ Thông báo
+ Mặc định
+ Cài đặt root
+ Các tùy chọn này sẽ chỉ hoạt động trên các thiết bị root! Nếu bạn không biết root là gì hoặc thiết bị của bạn đã được root hay chưa, vui lòng đừng để lại đánh giá kém nếu chúng không hoạt động. :)
+ Yêu cầu quyền WRITE_SECURE_SETTINGS
+ Các tùy chọn này chỉ được bật nếu Key Mapper có quyền WRITE_SECURE_SETTINGS. Nhấp vào nút bên dưới để tìm hiểu cách cấp quyền.
+ Hỗ trợ Shizuku
+ Shizuku là ứng dụng cho phép Key Mapper thực hiện những việc mà chỉ ứng dụng hệ thống mới làm được. Bạn không cần sử dụng bàn phím Key Mapper chẳng hạn. Nhấn để tìm hiểu cách thiết lập tính năng này.
+ Hãy làm theo các bước sau để thiết lập Shizuku.
+ Tự động thay đổi bàn phím
+ Đây là những cài đặt thực sự hữu ích và bạn nên kiểm tra chúng!
+ Ghi nhật ký
+ Điều này có thể làm tăng độ trễ cho bản đồ chính của bạn, vì vậy chỉ bật tính năng này nếu bạn đang cố gắng gỡ lỗi ứng dụng hoặc được nhà phát triển yêu cầu.
+
+
+ Nhật ký thay đổi
+ Giấy phép
+ Chính sách bảo mật
+ Tín dụng
+ Mã nguồn
+ Hồ sơ GitHub của nhà phát triển
+ Đánh giá và bình luận
+ Chủ đề XDA
+ Phiên bản
+ Dịch
+ Máy chủ Discord
+ Kênh YouTube (Hướng dẫn)
+
+
+ Hiển thị hộp thoại âm lượng
+ Nhấn giữ
+ Rung
+ Hiển thị thông báo trên màn hình
+ Rung khi nhấn phím lần đầu và rung lại khi nhấn lâu.
+ Phát hiện kích hoạt khi màn hình tắt
+ Lặp lại
+ %dx
+ sau %dms
+ mỗi %dms
+ cho đến khi vuốt lại
+ cho đến khi nhấn lại
+ cho đến khi được thả ra
+ Lặp lại
+ Lặp lại cho đến khi được thả ra
+ Lặp lại cho đến khi nhấn lại
+ Giữ và giữ
+ Giữ cho đến khi nhấn lại
+ Cho phép phím hoạt động bình thường
+ Giữ cho đến khi vuốt lại
+ Cho phép các ứng dụng khác kích hoạt bản đồ phím này
+
+
+ Dịch vụ trợ năng đã được bật :)
+ Tất cả đều tốt. Key Mapper hiện có thể phát hiện các lần nhấn nút của bạn.
+ Khởi động lại dịch vụ trợ năng
+ Các dịch vụ trợ năng đã được bật nhưng điện thoại của bạn đã tắt hoặc bị hỏng. Khởi động lại nó.
+ Khởi động lại
+ Kích hoạt dịch vụ trợ năng
+ @string/accessibility_service_explanation
+ Dịch vụ trợ năng bị vô hiệu hóa
+ Bạn chỉ có thể ghi lại trình kích hoạt nếu dịch vụ trợ năng được bật.
+ Lưu ý từ nhà phát triển
+ Không có gì đảm bảo rằng mọi hành động sẽ hoạt động trên thiết bị của bạn và mọi nút đều có thể được phát hiện. Điều này là do Android có nhiều phiên bản khác nhau và các OEM có thể vô tình hoặc cố ý phá vỡ các tính năng. Nếu có điều gì đó không ổn, vui lòng thông báo cho nhà phát triển và không đánh giá ứng dụng kém vì sự cố thường nằm ngoài tầm kiểm soát của nhà phát triển. =)
+ Key Mapper có thể ngừng hoạt động ngẫu nhiên!
+ QUAN TRỌNG!!! Nhấn \"tắt\" để hy vọng ngăn Android dừng ứng dụng khi nó đang chạy nền. Giao diện OEM của bạn như MIUI hoặc Samsung Experience có thể có các tính năng \"tiết kiệm pin\" khác nên bạn PHẢI tắt chúng cho Key Mapper bằng cách làm theo hướng dẫn tại dontkillmyapp.com
+ Tối ưu hóa pin Android gốc đã tắt. Điều này không đủ tốt cho hầu hết các thiết bị, vì vậy hãy truy cập Dontkillmyapp.com để xem hướng dẫn cách tắt nhiều tính năng diệt ứng dụng hơn nữa trên thiết bị của bạn.
+ Tắt
+ Truy cập Dontkillmyapp.com
+ Ánh xạ lại các nút âm lượng?
+ Bạn có thể ánh xạ lại các nút âm lượng
+ Key Mapper cần có quyền truy cập Không làm phiền nếu bạn muốn các hành động thay đổi âm lượng và ánh xạ lại các nút âm lượng hoạt động.
+ Tất cả đều tốt! :)
+ Đóng góp
+ "Ứng dụng này là nguồn mở! Bạn có thể bắt đầu đóng góp bằng cách truy cập kho lưu trữ sds100/KeyMapper trên GitHub và bằng cách tham gia máy chủ Discord. Ngay cả khi bạn không thể viết mã, bạn vẫn có thể đóng góp bằng cách giúp đỡ người khác và thử nghiệm các tính năng mới nhất."
+ Nhấn vào dấu 3 chấm để thay đổi hành vi lặp lại và hơn thế nữa. Bạn có thể kiểm tra một hành động và sửa lỗi hành động bằng cách nhấn vào hành động đó.
+ Ánh xạ lại cử chỉ đọc dấu vân tay
+ Bạn có thể ánh xạ lại cử chỉ đọc dấu vân tay! :)
+ Bạn không thể ánh xạ lại cử chỉ đọc dấu vân tay!
+ Bạn cần bật dịch vụ trợ năng để Key Mapper có thể kiểm tra xem thiết bị của bạn có thể phát hiện cử chỉ vân tay hay không.
+ Thiết bị của bạn có thể phát hiện cử chỉ vân tay! Có một tab ở đầu màn hình chính để ánh xạ lại cử chỉ vân tay.
+ Thiết bị của bạn không cho phép ứng dụng của bên thứ 3 phát hiện cử chỉ vân tay! Nhà phát triển không thể làm gì về điều này. Một số thiết bị có cài đặt vuốt xuống trên đầu đọc dấu vân tay để mở ngăn thông báo và không cho phép ứng dụng bên thứ 3 phát hiện cử chỉ vân tay.
+ Cho phép
+ Bạn cần thiết lập lại một số cài đặt
+ Có vẻ như bạn đã sử dụng cài đặt để tự động thay đổi bàn phím hoặc hiển thị trình chọn phương thức nhập khi thiết bị Bluetooth kết nối và ngắt kết nối. Key Mapper hiện cho phép bạn sử dụng bất kỳ thiết bị nhập nào chứ không chỉ thiết bị Bluetooth. Không có cách nào để di chuyển cài đặt cũ theo cách mà chức năng mới sẽ hoạt động nên bạn sẽ phải chọn lại thiết bị.
+ Cấp phép cho Shizuku
+ Có vẻ như bạn đã cài đặt Shizuku. Bạn nên cấp quyền cho Key Mapper sử dụng Shizuku để Key Mapper có thể làm nhiều việc hơn mà không cần người dùng nhập liệu. Ví dụ như nhấn nút nhập liệu mà không cần bạn phải sử dụng \'bàn phím Key Mapper\'. Nhấn \'thêm thông tin\' để đọc tất cả các lợi ích. Nhấn \'cấp\' để cấp quyền.
+ Shizuku đã được phép!
+ Bạn đã cấp quyền thành công cho Key Mapper Shizuku.
+ Thông tin thêm
+ Cấp quyền
+ Shizuku chưa bắt đầu
+ Shizuku phải được khởi động trước khi bạn cấp quyền cho Key Mapper sử dụng. Nhấn vào \'Khởi chạy Shizuku\' để mở ứng dụng Shizuku để bạn có thể khởi động ứng dụng.
+ Cài đặt Shizuku
+ Cấp quyền thông báo
+ Một số tính năng của Key Mapper yêu cầu quyền đăng thông báo, ví dụ: có thông báo tạm dừng/tiếp tục các bản đồ chính của bạn. Nhấn \'Cấp quyền\' để cấp quyền.
+ Thông báo có thể được hiển thị!
+ Bạn đã cấp quyền Key Mapper có thể hiển thị thông báo.
+ Cấp quyền
+
+
+ Khả năng tiếp cận
+ Báo thức
+ DTMF
+ Âm nhạc
+ Thông báo
+ Chuông
+ Hệ thống
+ Cuộc gọi thoại
+ Chuông
+ Rung
+ Im lặng
+ Đằng trước
+ Back
+ Báo động
+ Sự ưu tiên
+ Không có gì
+
+
+ Tạm dừng ánh xạ
+ Tiếp tục ánh xạ
+ Dịch vụ bị vô hiệu hóa
+ Dịch vụ trợ năng Key Mapper bị vô hiệu hóa
+ Dịch vụ bàn phím Key Mapper bị tắt
+ Chuyển đổi bàn phím Key Mapper
+
+
+ Ctrl
+ Ctrl left
+ Ctrl right
+ Alt
+ Alt left
+ Alt right
+ Shift
+ Shift left
+ Shift right
+ Meta
+ Meta trái
+ Meta phải
+ Sym
+ Func
+ Caps Lock
+ Num Lock
+ Scroll Lock
+
+
+ Bạn phải gõ một phím!
+ Bạn phải có ít nhất một trình kích hoạt
+ Bạn phải chọn một hành động!
+ Phím tắt phải có tiêu đề!
+ Bạn phải sử dụng một trong các bàn phím Key Mapper để tác vụ này hoạt động!
+ Không thể tìm thấy lối tắt. Ứng dụng đã được cài đặt hay kích hoạt chưa?
+ Ứng dụng có tên gói %s chưa được cài đặt!
+ Ứng dụng chưa được cài đặt!
+ Ứng dụng đã bị vô hiệu hóa!
+ Ứng dụng %s đã bị tắt!
+ Bạn cần cấp quyền cho Key Mapper để sửa đổi cài đặt hệ thống.
+ Điều này đòi hỏi sự cho phép root!
+ Hành động này cần có sự cho phép của máy ảnh!
+ Yêu cầu Android %s hoặc mới hơn
+ Yêu cầu Android %s trở lên
+ Thiết bị của bạn không có camera.
+ Thiết bị của bạn không hỗ trợ NFC.
+ Thiết bị của bạn không có đầu đọc dấu vân tay.
+ Thiết bị của bạn không hỗ trợ WiFi.
+ Thiết bị của bạn không hỗ trợ Bluetooth.
+ Thiết bị của bạn không hỗ trợ thực thi chính sách thiết bị.
+ Thiết bị của bạn không có đèn flash của máy ảnh.
+ Thiết bị của bạn không có bất kỳ tính năng điện thoại nào.
+ Không tìm thấy cờ \"%s\"!
+ Không thể tìm thấy trang cài đặt bàn phím!
+ Key Mapper cần phải là quản trị viên thiết bị!
+ Đang ở chế độ Không làm phiền!
+ Key Mapper không có quyền sử dụng phím tắt đó
+ Ứng dụng cần có quyền thay đổi trạng thái Không làm phiền!
+ Hành động này cần có quyền đọc trạng thái điện thoại!
+ Không thể tìm thấy trang cấp phép WRITE_SETTINGS!
+ Lỗi mở lối tắt ứng dụng này
+ Không có ứng dụng nào được cài đặt có thể gửi email!
+ Đã cấp quyền thay đổi chế độ Không làm phiền!
+ Không thể tìm thấy cài đặt quyền truy cập Không làm phiền!
+ Key Mapper cần có quyền WRITE_SECURE_SETTINGS.
+ Không thể tìm thấy ứng dụng nào để mở URL đó
+ Không thực hiện được lệnh \"getevent\". Bạn có chắc là bạn đã root chưa?
+ Không có ứng dụng nào có thể bắt đầu cuộc gọi điện thoại này
+ Máy ảnh đang được sử dụng!
+ Máy ảnh đã bị ngắt kết nối!
+ Máy ảnh bị vô hiệu hóa!
+ Lỗi máy ảnh!
+ Số lượng camera tối đa được sử dụng!
+ Không thể truy cập vào máy ảnh!
+ Không có đèn flash phía trước
+ Không có đèn flash phía sau
+ Dịch vụ trợ năng phải được kích hoạt để ứng dụng này hoạt động!
+ Dịch vụ trợ năng cần được kích hoạt!
+ Dịch vụ trợ năng đã được kích hoạt!
+ Dịch vụ trợ năng cần được kích hoạt!
+ Dịch vụ trợ năng cần được khởi động lại!
+ Trình khởi chạy của bạn không hỗ trợ phím tắt.
+ Một số tính năng cần có quyền WRITE_SECURE_SETTINGS.
+ Đã cấp quyền WRITE_SECURE_SETTINGS.
+ Phương thức nhập đã chọn của bạn cần được bật để “Sự kiện chính”, “Khóa”, “Văn bản” và một số hành động khác hoạt động.
+ Cần phải bật bàn phím Key Mapper!
+ Bàn phím Key Mapper đã được bật!
+ Bàn phím Key Mapper phải được bật và chọn để một số hành động của bạn hoạt động!
+ Không thể tìm thấy phương thức nhập %s
+ Không thể hiển thị bộ chọn phương thức nhập!
+ Không tìm thấy nút trợ năng!
+ Không thực hiện được hành động chung %s!
+ Bản đồ chính của bạn sẽ không hoạt động! Một số điều cần sửa chữa!
+ Bạn không thể di chuyển đến cuối văn bản trong trường này!
+ Không tìm thấy cài đặt tối ưu hóa pin! Nếu nó tồn tại, hãy mở nó bằng tay.
+ Không tìm thấy phần bổ sung (%s)!
+ Bạn không thể có những ràng buộc trùng lặp!
+ Cử chỉ này đã có hạn chế này!
+ Không thể trống rỗng!
+ Điều này không được hỗ trợ. :(
+ Không tìm thấy thiết bị!
+ Không thể chọn tập tin
+ Tệp JSON trống!
+ Quyền truy cập tập tin bị từ chối! %S
+ Lỗi IO không xác định!
+ Đã hủy!
+ Không thể sao lưu vào tập tin. Nó có bị xóa không?
+ Số không hợp lệ!
+ Ít nhất phải là %s!
+ Tối đa phải là %s!
+ Tối ưu hóa pin được bật! Hãy tắt tính năng này đi vì điều này có thể khiến Key Mapper ngừng hoạt động một cách ngẫu nhiên.
+ Điều này đòi hỏi sự cho phép root!
+ Không thể tìm thấy cài đặt truy cập thông báo!
+ Quyền truy cập thông báo bị từ chối!
+ Không hợp lệ!
+ Bị từ chối quyền bắt đầu cuộc gọi điện thoại!
+ Bạn sẽ cần cập nhật Key Mapper lên phiên bản mới nhất để sử dụng bản sao lưu này.
+ Tệp JSON bị hỏng!
+ Không có trợ lý giọng nói được cài đặt!
+ Không đủ quyền
+ Bạn chỉ cài đặt bàn phím Key Mapper!
+ Không có ứng dụng chơi phương tiện truyền thông!
+ Không tìm thấy tập tin nguồn! %S
+ Không tìm thấy tệp mục tiêu! %S
+ Không nhập được cử chỉ!
+ Không thể sửa đổi cài đặt hệ thống %s!
+ Bạn cần kích hoạt %s!
+ Không thể thay đổi ime!
+ Thiết bị của bạn không có ứng dụng camera!
+ Thiết bị của bạn không có trợ lý!
+ Thiết bị của bạn không có ứng dụng cài đặt!
+ Không ứng dụng nào có thể mở url này!
+ Không tạo được tập tin!
+ Không phải là một thư mục! %S
+ Không phải là một tập tin! %S
+ Không tìm thấy thư mục! %S
+ Không thể tìm thấy tập tin âm thanh!
+ Quyền lưu trữ bị từ chối!
+ Nguồn và đích không thể giống nhau!
+ Không còn khoảng trống ở mục tiêu! %S
+ Sự cho phép của Shizuku bị từ chối!
+ Shizuku chưa bắt đầu!
+ Tập tin này không có tên!
+ Bạn phải cấp quyền Key Mapper để xem các thiết bị Bluetooth đã ghép nối của mình.
+ Bị từ chối quyền đọc vị trí !
+ Bị từ chối quyền trả lời và kết thúc cuộc gọi điện thoại!
+ Đã từ chối quyền xem các thiết bị Bluetooth được ghép nối!
+ Đã từ chối quyền hiển thị thông báo!
+ Phải từ 2 trở lên!
+ Phải bằng %d hoặc ít hơn!
+ Phải lớn hơn 0!
+ Phải lớn hơn 0!
+ Phải lớn hơn 0!
+ Phải lớn hơn 0!
+ Phải bằng %d hoặc ít hơn!
+
+
+ Chuyển đổi Wi-Fi
+ Bật Wi-Fi
+ Tắt Wi-Fi
+ Chuyển đổi Bluetooth
+ Bật Bluetooth
+ Tắt Bluetooth
+ Tăng âm lượng
+ Giảm âm lượng
+ Tắt âm lượng
+ Chuyển đổi tắt tiếng
+ Bật âm lượng
+ Hiển thị hộp thoại âm lượng
+ Tăng luồng
+ Tăng luồng %s
+ Giảm luồng
+ Giảm luồng %s
+ Chuyển qua các chế độ chuông (Rung, Rung, Im lặng)
+ Chuyển qua các chế độ chuông (Ring, Vibrate)
+ Thay đổi chế độ chuông
+ Thay đổi sang chế độ %s
+ Chuyển đổi chế độ Không làm phiền
+ Chỉ chuyển đổi chế độ DND %s
+ Bật chế độ Không làm phiền
+ Chỉ bật chế độ DND %s
+ Tắt chế độ Không làm phiền
+ Bật tự động xoay
+ Vô hiệu hóa tự động xoay
+ Chuyển đổi tự động xoay
+ Màn hình dọc
+ Màn hình ngang
+ Xoay màn hình
+ Cài đặt xoay màn hình
+ Xoay vòng qua %s vòng quay
+ Chuyển đổi dữ liệu di động
+ Kích hoạt dữ liệu di động
+ Tắt dữ liệu di động
+ Chuyển đổi độ sáng tự động
+ Tắt độ sáng tự động
+ Bật độ sáng tự động
+ Tăng độ sáng màn hình
+ Giảm độ sáng màn hình
+ Mở rộng ngăn thông báo
+ Chuyển đổi ngăn thông báo
+ Mở rộng cài đặt nhanh
+ Chuyển đổi ngăn cài đặt nhanh
+ Thu gọn thanh trạng thái
+ Tạm dừng phát lại phương tiện
+ Tạm dừng phát lại phương tiện cho một ứng dụng
+ Tạm dừng phương tiện trong %s
+ Tiếp tục phát lại phương tiện
+ Tiếp tục phát lại phương tiện cho một ứng dụng
+ Tiếp tục phương tiện cho %s
+ Phát/Tạm dừng phát lại phương tiện
+ Phát/Tạm dừng phát lại phương tiện cho một ứng dụng
+ Phát/Tạm dừng phương tiện trong %s
+ Bài hát tiếp theo
+ Bản nhạc tiếp theo cho một ứng dụng
+ Bài hát tiếp theo cho %s
+ Bản nhạc trước
+ Bản nhạc trước của một ứng dụng
+ Bài hát trước đó của %s
+ Chuyển tiếp nhanh
+ Chuyển tiếp nhanh cho một ứng dụng
+ Chuyển tiếp nhanh trong %s
+ Không phải tất cả các ứng dụng đa phương tiện đều hỗ trợ chuyển tiếp nhanh. Ví dụ: Google Play Âm nhạc.
+ Tua lại
+ Tua lại cho một ứng dụng
+ Tua lại trong %s
+ Không phải tất cả các ứng dụng media đều hỗ trợ tua lại. Ví dụ: Google Play Âm nhạc.
+ Quay lại
+ Màn hình chính
+ Mở gần đây
+ Mở trình đơn
+ Chuyển đổi màn hình chia nhỏ
+ Chuyển đến ứng dụng cuối cùng. (Nhấn đúp vào phần gần đây)
+ Bật tắt đèn pin
+ Bật đèn pin
+ Tắt đèn pin
+ Chuyển đổi đèn pin %s
+ Bật đèn pin %s
+ Tắt đèn pin %s
+ Bật NFC
+ Tắt NFC
+ Chuyển đổi NFC
+ Chụp màn hình
+ Khởi chạy trợ lý giọng nói
+ Khởi chạy trợ lý thiết bị
+ Mở máy ảnh
+ Khóa thiết bị
+ Thiết bị khóa an toàn
+ Bạn sẽ chỉ có thể đăng nhập lại bằng mã PIN của mình. Máy quét dấu vân tay và mở khóa bằng khuôn mặt sẽ bị tắt. Đây là cách đáng tin cậy duy nhất mà tôi tìm thấy để khóa các thiết bị chưa root trước Android 9.0.
+ Thiết bị ngủ/thức
+ Bạn phải bật tùy chọn phát hiện trigger khi màn hình tắt!
+ Không làm gì cả
+ Di chuyển con trỏ đến cuối
+ Hành động này có thể không hoạt động như dự định trong một số ứng dụng.
+ Chuyển đổi bàn phím
+ Hành động này sẽ chỉ hoạt động nếu bạn đã nhấn vào trường nhập liệu nơi bàn phím được cho là sẽ hiển thị.
+ Hiển thị bàn phím
+ Ẩn bàn phím
+ Hiển thị hộp chọn bàn phím
+ Chuyển đổi bàn phím thiết lập
+ Chuyển sang %s
+ Cắt
+ Sao chép
+ Dán
+ Chọn từ tại con trỏ
+ Mở cài đặt
+ Hiển thị menu nguồn
+ Chuyển đổi chế độ máy bay
+ Bật chế độ máy bay
+ Tắt chế độ Máy bay
+ Khởi chạy ứng dụng
+ Khởi chạy phím tắt ứng dụng
+ Nhập mã khóa
+ Sự kiện phím đầu vào
+ Nhấn vào màn hình
+ Vuốt màn hình
+ Chụm màn hình
+ Nhập văn bản
+ Mở URL
+ Gửi thông tin
+ Bắt đầu cuộc gọi điện thoại
+ Trả lời cuộc gọi điện thoại
+ Kết thúc cuộc gọi điện thoại
+ Phát âm thanh
+ Loại bỏ thông báo gần đây nhất
+ Loại bỏ tất cả thông báo
+
+
+ Điều hướng
+ Âm lượng
+ Phương tiện truyền thông
+ Bàn phím
+ Ứng dụng
+ Đầu vào
+ Máy ảnh & Âm thanh
+ Kết nối
+ Nội dung
+ Giao diện
+ Điện thoại
+ Hiển thị
+ Thông báo
+
+
+ Boolean
+ Boolean array
+ Số nguyên
+ Integer array
+ Chuỗi
+ Mảng chuỗi
+ Dài
+ Mảng dài
+ Byte
+ Mảng byte
+ Double
+ Mảng đôi
+ Ký tự
+ Mảng ký tự
+ Nổi
+ Mảng nổi
+ Ngắn
+ Mảng ngắn
+ Chỉ có thể là \"đúng\" hoặc \"sai\"
+ Danh sách \"đúng\" và \"sai\" được phân tách bằng dấu phẩy. Ví dụ: đúng, sai, đúng
+ Một số nguyên hợp lệ trong ngôn ngữ lập trình Java.
+ Danh sách các số nguyên hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 100,399
+ Một danh sách được phân tách bằng dấu phẩy. Ví dụ: loại 1, loại 2
+ Bất kỳ văn bản nào.
+ Danh sách các chuỗi được phân tách bằng dấu phẩy. Ví dụ: chuỗi1, chuỗi2
+ Một Long hợp lệ trong ngôn ngữ lập trình Java.
+ Danh sách các Độ dài hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 102302234234234,399083423234429
+ Một Byte hợp lệ trong ngôn ngữ lập trình Java.
+ Danh sách các Byte hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 123,3
+ Một Double hợp lệ trong ngôn ngữ lập trình Java.
+ Danh sách các Mảng đôi hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 1.0,3.234
+ Một Char hợp lệ trong ngôn ngữ lập trình Java. Ví dụ: \'a\' hoặc \'b\'
+ Danh sách các ký tự hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: a,b,c
+ Float hợp lệ trong ngôn ngữ lập trình Java. Ví dụ: 3,145
+ Danh sách các Mảng nổi hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 1241.123
+ Một đoạn ngắn hợp lệ bằng ngôn ngữ lập trình Java. Ví dụ: 2342
+ Danh sách các Mảng ngắn hợp lệ được phân tách bằng dấu phẩy bằng ngôn ngữ lập trình Java. Ví dụ: 3242,12354
+ Các cờ cho một Ý định được lưu dưới dạng cờ bit. Những cờ này thay đổi cách xử lý Ý định. Nếu mục này trống đối với Ý định hoạt động thì Trình ánh xạ khóa sẽ sử dụng FLAG_ACTIVITY_NEW_TASK theo mặc định. Để biết thêm thông tin, hãy nhấn vào \'tài liệu\' để xem tài liệu dành cho nhà phát triển Android.
+
+
+ Hướng dẫn bắt đầu nhanh
+ Hãy xem Hướng dẫn bắt đầu nhanh nếu bạn gặp khó khăn.
+
+
+ GitHub
+ Website
+ Bản dịch
+ Phiên bản %s
+ Đánh giá
+ Nhật ký thay đổi
+ Discord
+ Thông tin pháp lý
+ Giấy phép
+ Giấy phép nguồn mở cho ứng dụng này.
+ Chính sách bảo mật
+ Chúng tôi không thu thập bất kỳ thông tin cá nhân nào nhưng đây là chính sách bảo mật nói lên điều này.
+ Đội ngũ của chúng tôi
+ Nhà phát triển
+ Người điều hành/hỗ trợ cộng đồng
+ Người điều hành/hỗ trợ cộng đồng
+ Phiên dịch viên (tiếng Ba Lan)
+ Người phiên dịch (tiếng Séc)
+ Phiên dịch viên (tiếng Tây Ban Nha)
+
+
+
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 0f9249a0bb..25073c5956 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -2,12 +2,11 @@
松开你的按键!未选择动作
- Key Mapper 需要使用无障碍服务,以便它可以检测并更改您在应用程序之外时按下的按钮的操作。只有在您启用了无障碍服务后,您的按键映射才会起作用。必须打开无障碍服务才能创建触发器。
+ 键映射器需要使用无障碍服务,以便它可以检测并更改您在应用程序之外时按下的按钮的操作。只有在您启用了无障碍服务后,您的按键映射才会起作用。必须打开无障碍服务才能创建触发器。已选择 %d 个启用打开¯\\_(ツ)_/¯\n\n这里什么都没有!
- 录制触发器!添加动作!¯\\_(ツ)_/¯\n\n创建一个按键映射!¯\\_(ツ)_/¯\n\n你没有为此快捷方式选择任何动作!
@@ -42,7 +41,7 @@
显示隐藏的应用修改器切换
- 重要!!!这些坐标只有当你的显示方向与屏幕截图相同时才是正确的! 此操作将取消您在屏幕上所做的任何触摸或手势。\n\n如果你寻找您屏幕上某点的坐标需要帮助,就采取屏幕截图的方法,然后在屏幕截图上点击您想让此动作按下的地方。
+ 重要的提示!! 仅当你的屏幕与截图的方向相同时,这些坐标才是正确的! 此操作将取消你在屏幕上执行的任何触摸或手势。\n\n如果您需要帮助查找屏幕上某个点的坐标,请截屏后点击截图上您想让此动作按下的地方。注意:在使用 “缩小” 时,X和Y是结束坐标,在使用 “放大” 时,X和Y是起始坐标。未知设备名称!点击动作以修复!
@@ -176,10 +175,6 @@
动作触发器指纹
-
- 按键事件
- 指纹
-
@@ -1074,4 +1069,6 @@
译者(捷克语)译者(西班牙语)
+
+
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 8b2b1737b0..aef0a460ed 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -63,4 +63,6 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3faa26d102..5ff6871c3c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -10,7 +10,7 @@
Open¯\\_(ツ)_/¯¯\\_(ツ)_/¯\n\nNothing here!
- Record a trigger!
+ The first step is to add some buttons that will trigger the key map.\n\nFirst tap ‘Record trigger’ and then press the buttons that you want to remap. They will appear here.\n\nAlternatively, you can trigger a key map using an ‘advanced trigger’.Add an action!¯\\_(ツ)_/¯\n\nCreate a key map!¯\\_(ツ)_/¯\n\nYou haven\'t chosen any actions for this shortcut!
@@ -209,11 +209,6 @@
TriggerKey eventsFingerprint
-
-
- @string/tab_keyevents
- @string/tab_fingerprint
-
@@ -383,6 +378,7 @@
https://github.com/sds100/KeyMapperhttps://play.google.com/store/apps/details?id=io.github.sds100.keymapperhttps://keymapperorg.github.io/KeyMapper
+ https://keymapperorg.github.io/KeyMapper/redirects/advanced-triggershttps://play.google.com/store/apps/details?id=io.github.sds100.keymapper.inputmethod.latinhttps://f-droid.org/en/packages/io.github.sds100.keymapper.inputmethod.latin
@@ -453,6 +449,9 @@
Add actionRecord trigger
+ Advanced triggers
+ NEW!
+ Dismiss choosing advanced triggers.DoneSaveFix
@@ -1374,5 +1373,43 @@
Darío B. C. (bydariogamer)https://github.com/bydariogamerTranslator (Spanish)
+
+
+
+ Any assistant
+ Device assistant
+ Voice assistant
+ Advanced triggers
+ The developer doesn\'t believe ads are a sustainable or user-friendly form of monetization so these paid triggers help support development ❤️. You will be given priority support as well.
+ Assistant trigger
+ Did you know you can remap your device assistant? Instead of launching Google Assistant or Bixby, your device can perform an action of your choice. It works even when the screen is off!
+ Thank you for supporting the app! ❤️
+ Your purchase was successful and you can now use the assistant triggers. As a paying user of Key Mapper you will receive priority support to help you use the app. There is now a button on this page to contact the developer.
+ You must select Key Mapper as the default digital assistant app for this trigger to work.
+ Key Mapper must be the default assistant.
+ You must purchase the assistant trigger feature!
+ Learn more
+ New trigger! Did you know you can remap your digital assistant?
+ Instead of launching Google Assistant or Bixby, you can perform an action of your choice. Tap to learn more.
+
+
+ Unlock (%s)
+ Use
+ Loading…
+ Retry fetching price
+ Purchase cancelled.
+ This requires a paid feature that can only be bought by downloading Key Mapper from Google Play.
+ Network error encountered. Do you have an internet connection?
+ This product was not found.
+ Google Play encountered an error.
+ Something went wrong 😕
+ Retry
+ Contact developer
+ You must purchase the assistant trigger feature! Tap on the key map and then purchase it by clicking on \'Advanced triggers\'.
+ contact@keymapper.club
+ Key Mapper Pro query
+ Please describe the problem you are having here.
+ The advanced triggers are paid feature but you downloaded the FOSS build of Key Mapper that does not include this module or Google Play billing. Please download Key Mapper from Google Play to get access to this feature.
+ Download Play build
diff --git a/app/src/test/java/io/github/sds100/keymapper/AutoSwitchImeControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/AutoSwitchImeControllerTest.kt
deleted file mode 100644
index c906eb2d3a..0000000000
--- a/app/src/test/java/io/github/sds100/keymapper/AutoSwitchImeControllerTest.kt
+++ /dev/null
@@ -1,224 +0,0 @@
-package io.github.sds100.keymapper
-
-import io.github.sds100.keymapper.data.Keys
-import io.github.sds100.keymapper.data.repositories.FakePreferenceRepository
-import io.github.sds100.keymapper.mappings.PauseMappingsUseCase
-import io.github.sds100.keymapper.system.devices.FakeDevicesAdapter
-import io.github.sds100.keymapper.system.devices.InputDeviceInfo
-import io.github.sds100.keymapper.system.inputmethod.AutoSwitchImeController
-import io.github.sds100.keymapper.system.inputmethod.ImeInfo
-import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter
-import io.github.sds100.keymapper.system.popup.PopupMessageAdapter
-import io.github.sds100.keymapper.util.ServiceEvent
-import io.github.sds100.keymapper.util.Success
-import io.github.sds100.keymapper.util.ui.ResourceProvider
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.junit.MockitoJUnitRunner
-import org.mockito.kotlin.anyOrNull
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.times
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
-
-/**
- * Created by sds100 on 25/04/2021.
- */
-
-@ExperimentalCoroutinesApi
-@RunWith(MockitoJUnitRunner::class)
-class AutoSwitchImeControllerTest {
-
- companion object {
- private const val KEY_MAPPER_IME_ID = "key_mapper_keyboard_id"
- private const val NORMAL_IME_ID = "proper_keyboard_id"
-
- private val FAKE_KEYBOARD = InputDeviceInfo(
- descriptor = "fake_keyboard_descriptor",
- name = "fake keyboard",
- id = 1,
- isExternal = true,
- isGameController = false,
- )
-
- private val FAKE_CONTROLLER = InputDeviceInfo(
- descriptor = "fake_controller_descriptor",
- name = "fake controller",
- id = 2,
- isExternal = true,
- isGameController = true,
- )
-
- private val KEY_MAPPER_IME = ImeInfo(
- id = KEY_MAPPER_IME_ID,
- packageName = Constants.PACKAGE_NAME,
- label = "label",
- isEnabled = true,
- isChosen = false,
- )
-
- private val NORMAL_IME = ImeInfo(
- id = NORMAL_IME_ID,
- packageName = "other.example.app",
- label = "normal keyboard",
- isEnabled = true,
- isChosen = true,
- )
- }
-
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
-
- private lateinit var controller: AutoSwitchImeController
- private lateinit var fakePreferenceRepository: FakePreferenceRepository
- private lateinit var mockInputMethodAdapter: InputMethodAdapter
- private lateinit var mockPauseMappingsUseCase: PauseMappingsUseCase
- private lateinit var fakeDevicesAdapter: FakeDevicesAdapter
- private lateinit var mockPopupMessageAdapter: PopupMessageAdapter
- private lateinit var mockResourceProvider: ResourceProvider
-
- @Before
- fun init() {
- fakePreferenceRepository = FakePreferenceRepository()
-
- mockInputMethodAdapter = mock {
- on { getInfoByPackageName(Constants.PACKAGE_NAME) }.then {
- Success(KEY_MAPPER_IME)
- }
-
- on { inputMethodHistory }.then {
- MutableStateFlow(
- listOf(NORMAL_IME),
- )
- }
-
- onBlocking { chooseImeWithoutUserInput(KEY_MAPPER_IME_ID) }.then {
- Success(
- KEY_MAPPER_IME,
- )
- }
- onBlocking { chooseImeWithoutUserInput(NORMAL_IME_ID) }.then {
- Success(
- NORMAL_IME,
- )
- }
- }
-
- fakeDevicesAdapter = FakeDevicesAdapter()
-
- mockPopupMessageAdapter = mock()
-
- mockPauseMappingsUseCase = mock {
- on { isPaused }.then { flow { } }
- }
-
- mockResourceProvider = mock()
-
- controller = AutoSwitchImeController(
- coroutineScope,
- fakePreferenceRepository,
- mockInputMethodAdapter,
- mockPauseMappingsUseCase,
- fakeDevicesAdapter,
- mockPopupMessageAdapter,
- mockResourceProvider,
- accessibilityServiceAdapter = mock {
- on { eventReceiver }.then { MutableSharedFlow() }
- },
- )
- }
-
- @Test
- fun `choose single device, when device connected, show ime picker`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val chosenDevices = setOf(FAKE_KEYBOARD.descriptor)
-
- fakePreferenceRepository.set(Keys.showImePickerOnDeviceConnect, true)
- fakePreferenceRepository.set(Keys.devicesThatShowImePicker, chosenDevices)
-
- // WHEN
- fakeDevicesAdapter.onInputDeviceConnect.emit(FAKE_KEYBOARD)
-
- // THEN
- verify(mockInputMethodAdapter, times(1)).showImePicker(fromForeground = false)
- }
-
- @Test
- fun `choose single device, when device disconnected, show ime picker`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val chosenDevices = setOf(FAKE_KEYBOARD.descriptor)
-
- fakePreferenceRepository.set(Keys.showImePickerOnDeviceConnect, true)
- fakePreferenceRepository.set(Keys.devicesThatShowImePicker, chosenDevices)
-
- // WHEN
- fakeDevicesAdapter.onInputDeviceDisconnect.emit(FAKE_KEYBOARD)
-
- // THEN
- verify(mockInputMethodAdapter, times(1)).showImePicker(fromForeground = false)
- }
-
- @Test
- fun `choose single device, on device disconnect, choose normal keyboard`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val chosenDevices = setOf(FAKE_KEYBOARD.descriptor)
- fakePreferenceRepository.set(Keys.devicesThatChangeIme, chosenDevices)
- fakePreferenceRepository.set(Keys.changeImeOnDeviceConnect, true)
- fakePreferenceRepository.set(Keys.showToastWhenAutoChangingIme, true)
-
- whenever(mockInputMethodAdapter.chosenIme).then { MutableStateFlow(KEY_MAPPER_IME) }
-
- // WHEN
- fakeDevicesAdapter.onInputDeviceDisconnect.emit(FAKE_KEYBOARD)
-
- // THEN
- verify(mockInputMethodAdapter, times(1)).chooseImeWithoutUserInput(
- NORMAL_IME_ID,
- )
-
- verify(mockResourceProvider, times(1)).getString(
- R.string.toast_chose_keyboard,
- NORMAL_IME.label,
- )
- verify(mockPopupMessageAdapter, times(1)).showPopupMessage(anyOrNull())
- }
-
- @Test
- fun `choose single device, when device connected, choose key mapper keyboard`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val chosenDevices = setOf(FAKE_KEYBOARD.descriptor)
- fakePreferenceRepository.set(Keys.devicesThatChangeIme, chosenDevices)
- fakePreferenceRepository.set(Keys.changeImeOnDeviceConnect, true)
- fakePreferenceRepository.set(Keys.showToastWhenAutoChangingIme, true)
-
- whenever(mockInputMethodAdapter.chosenIme).then { MutableStateFlow(NORMAL_IME) }
-
- // WHEN
- fakeDevicesAdapter.onInputDeviceConnect.emit(FAKE_KEYBOARD)
-
- // THEN
- verify(mockInputMethodAdapter, times(1)).chooseImeWithoutUserInput(
- KEY_MAPPER_IME_ID,
- )
-
- verify(mockResourceProvider, times(1)).getString(
- R.string.toast_chose_keyboard,
- KEY_MAPPER_IME.label,
- )
- verify(mockPopupMessageAdapter, times(1)).showPopupMessage(anyOrNull())
- }
-}
diff --git a/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt b/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt
index 3deb16da96..2874f2287d 100644
--- a/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt
@@ -25,12 +25,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.test.DelayController
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
@@ -51,7 +50,6 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import timber.log.Timber
import java.io.File
-import kotlin.coroutines.ContinuationInterceptor
/**
* Created by sds100 on 19/04/2021.
@@ -65,9 +63,8 @@ class BackupManagerTest {
@get:Rule
var temporaryFolder = TemporaryFolder()
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private val dispatcherProvider = TestDispatcherProvider(testDispatcher)
@@ -83,7 +80,9 @@ class BackupManagerTest {
private lateinit var gson: Gson
@Before
- fun init() {
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+
Timber.plant(TestLoggingTree())
fakePreferenceRepository = FakePreferenceRepository()
@@ -105,7 +104,7 @@ class BackupManagerTest {
mockUuidGenerator = mock()
backupManager = BackupManagerImpl(
- coroutineScope,
+ testScope,
fileAdapter = fakeFileAdapter,
keyMapRepository = mockKeyMapRepository,
preferenceRepository = fakePreferenceRepository,
@@ -118,14 +117,11 @@ class BackupManagerTest {
parser = JsonParser()
gson = Gson()
-
- Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
}
/**
@@ -133,7 +129,9 @@ class BackupManagerTest {
*/
@Test
fun `Don't allow back ups from a newer version of key mapper`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
+ advanceUntilIdle()
+
// GIVEN
val dataJsonFile = "restore-app-version-too-big.zip/data.json"
val zipFile = fakeFileAdapter.getPrivateFile("backup.zip")
@@ -141,14 +139,11 @@ class BackupManagerTest {
copyFileToPrivateFolder(dataJsonFile, destination = "backup.zip/data.json")
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.restoreMappings(zipFile.uri)
+ advanceUntilIdle()
// THEN
assertThat(result, `is`(Error.BackupVersionTooNew))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
}
/**
@@ -156,7 +151,7 @@ class BackupManagerTest {
*/
@Test
fun `Allow back ups from a back up without a key mapper version in it`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
whenever(mockKeyMapRepository.keyMapList).then {
MutableStateFlow(State.Data(emptyList()))
@@ -172,18 +167,15 @@ class BackupManagerTest {
copyFileToPrivateFolder(dataJsonFile, destination = "backup.zip/data.json")
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
val result = backupManager.restoreMappings(zipFile.uri)
// THEN
assertThat(result, `is`(Success(Unit)))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
}
@Test
- fun `don't crash if back up does not contain sounds folder`() = coroutineScope.runBlockingTest {
+ fun `don't crash if back up does not contain sounds folder`() = runTest(testDispatcher) {
// GIVEN
whenever(mockKeyMapRepository.keyMapList).then {
MutableStateFlow(State.Data(emptyList()))
@@ -199,19 +191,15 @@ class BackupManagerTest {
copyFileToPrivateFolder(dataJsonFile, destination = "backup.zip/data.json")
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.restoreMappings(zipFile.uri)
// THEN
assertThat(result, `is`(Success(Unit)))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
}
@Test
fun `successfully restore zip folder with data json and sound files`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val dataJsonFile = "restore-all.zip/data.json"
val soundFile = "restore-all.zip/sounds/sound.ogg"
@@ -221,15 +209,11 @@ class BackupManagerTest {
copyFileToPrivateFolder(soundFile, destination = "backup.zip/sounds/sound.ogg")
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.restoreMappings(zipFile.uri)
// THEN
assertThat(result, `is`(Success(Unit)))
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
verify(mockKeyMapRepository, times(1)).insert(any(), any())
verify(mockFingerprintMapRepository, times(1)).update(any(), any(), any(), any())
verify(mockSoundsManager, times(1)).restoreSound(any())
@@ -240,7 +224,7 @@ class BackupManagerTest {
*/
@Test
fun `backup sound file even if there is not a key map with a sound action`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val backupDirUuid = "backup_uid"
val soundFileName = "sound.ogg"
@@ -270,19 +254,14 @@ class BackupManagerTest {
}
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val backupZip = File(temporaryFolder.root, "backup.zip")
backupZip.mkdirs()
val result = backupManager.backupMappings(uri = backupZip.path)
// THEN
-
assertThat(result, `is`(Success(backupZip.path)))
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
// only 2 files have been backed up
assertThat(backupZip.listFiles()?.size, `is`(2))
@@ -294,7 +273,7 @@ class BackupManagerTest {
@Test
fun `backup sound file if there is a key map with a sound action`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val backupDirUuid = "backup_uuid"
val soundFileUid = "uid"
@@ -328,8 +307,6 @@ class BackupManagerTest {
soundFile.createFile()
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val backupZip = File(temporaryFolder.root, "backup.zip")
backupZip.mkdirs()
@@ -339,8 +316,6 @@ class BackupManagerTest {
assertThat(result, `is`(Success(backupZip.path)))
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
// only 2 files have been backed up
assertThat(backupZip.listFiles()?.size, `is`(2))
@@ -352,145 +327,106 @@ class BackupManagerTest {
}
@Test
- fun `restore legacy backup with device info, success`() = coroutineScope.runBlockingTest {
+ fun `restore legacy backup with device info, success`() = runTest(testDispatcher) {
// GIVEN
val fileName = "legacy-backup-test-data.json"
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
// THEN
assertThat(result, `is`(Success(Unit)))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
verify(mockKeyMapRepository, times(1)).insert(any(), any())
verify(mockFingerprintMapRepository, times(1)).update(any(), any(), any(), any())
}
@Test
fun `restore keymaps with no db version, assume version is 9 and don't show error message`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val fileName = "restore-keymaps-no-db-version.json"
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
assertThat(result, `is`(Success(Unit)))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
verify(mockKeyMapRepository, times(1)).insert(any(), any())
}
@Test
fun `restore a single legacy fingerprint map, only restore a single fingerprint map and a success message`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val fileName = "restore-legacy-single-fingerprint-map.json"
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
assertThat(result, `is`(Success(Unit)))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
verify(mockFingerprintMapRepository, times(1)).update(any())
}
@Test
fun `restore all legacy fingerprint maps, all fingerprint maps should be restored and a success message`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val fileName = "restore-all-legacy-fingerprint-maps.json"
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
assertThat(result, `is`(Success(Unit)))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
verify(mockFingerprintMapRepository, times(1)).update(any(), any(), any(), any())
}
@Test
fun `restore many key maps and device info, all key maps and device info should be restored and a success message`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val fileName = "restore-many-keymaps.json"
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
assertThat(result, `is`(Success(Unit)))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
verify(mockKeyMapRepository, times(1)).insert(any(), any(), any(), any())
}
@Test
fun `restore with key map db version greater than allowed version, send incompatible backup event`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val fileName = "restore-keymap-db-version-too-big.json"
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
assertThat(result, `is`(Error.BackupVersionTooNew))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
verify(mockKeyMapRepository, never()).insert(anyVararg())
}
@Test
fun `restore with legacy fingerprint gesture map db version greater than allowed version, send incompatible backup event`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val fileName = "restore-legacy-fingerprint-map-version-too-big.json"
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
assertThat(result, `is`(Error.BackupVersionTooNew))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
verify(mockFingerprintMapRepository, never()).update(anyVararg())
}
@Test
- fun `restore empty file, show empty json error message`() = coroutineScope.runBlockingTest {
+ fun `restore empty file, show empty json error message`() = runTest(testDispatcher) {
val fileName = "empty.json"
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
assertThat(result, `is`(Error.EmptyJson))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
}
@Test
- fun `restore corrupt file, show corrupt json message`() = coroutineScope.runBlockingTest {
+ fun `restore corrupt file, show corrupt json message`() = runTest(testDispatcher) {
val fileName = "corrupt.json"
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName))
assertThat(result, IsInstanceOf(Error.CorruptJsonFile::class.java))
-
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
}
@Test
fun `backup all fingerprint maps, return list of fingerprint maps and app database version`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val backupDirUuid = "backup_uuid"
@@ -513,15 +449,11 @@ class BackupManagerTest {
backupZip.mkdirs()
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.backupFingerprintMaps(backupZip.path)
// THEN
assertThat(result, `is`(Success(backupZip.path)))
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
// only 1 file has been backed up
assertThat(backupZip.listFiles()?.size, `is`(1))
@@ -543,9 +475,8 @@ class BackupManagerTest {
@Test
fun `backup key maps, return list of default key maps, keymap db version should be current database version`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
-
val backupDirUuid = "backup_uuid"
whenever(mockUuidGenerator.random()).then {
@@ -560,15 +491,11 @@ class BackupManagerTest {
backupZip.mkdirs()
// WHEN
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
-
val result = backupManager.backupKeyMaps(backupZip.path, keyMapList.map { it.uid })
// THEN
assertThat(result, `is`(Success(backupZip.path)))
- (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
// only 1 file has been backed up
assertThat(backupZip.listFiles()?.size, `is`(1))
diff --git a/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt
index 67b541875c..cfd6c56ac3 100644
--- a/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt
@@ -3,24 +3,29 @@ package io.github.sds100.keymapper
import android.view.KeyEvent
import io.github.sds100.keymapper.actions.ActionData
import io.github.sds100.keymapper.constraints.Constraint
+import io.github.sds100.keymapper.mappings.ClickType
import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCaseImpl
import io.github.sds100.keymapper.mappings.keymaps.KeyMap
import io.github.sds100.keymapper.mappings.keymaps.KeyMapAction
+import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerType
+import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice
+import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode
import io.github.sds100.keymapper.system.keyevents.KeyEventUtils
import io.github.sds100.keymapper.util.State
import io.github.sds100.keymapper.util.dataOrNull
import io.github.sds100.keymapper.util.singleKeyTrigger
import io.github.sds100.keymapper.util.triggerKey
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.hasSize
+import org.hamcrest.Matchers.instanceOf
import org.hamcrest.Matchers.`is`
-import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
@@ -32,9 +37,7 @@ import org.mockito.kotlin.mock
@ExperimentalCoroutinesApi
class ConfigKeyMapUseCaseTest {
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
private lateinit var useCase: ConfigKeyMapUseCaseImpl
@@ -47,10 +50,104 @@ class ConfigKeyMapUseCaseTest {
)
}
- @After
- fun tearDown() {
- testDispatcher.cleanupTestCoroutines()
- }
+ /**
+ * This ensures that it isn't possible to have two or more assistant triggers when the mode is parallel.
+ */
+ @Test
+ fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() =
+ runTest(testDispatcher) {
+ useCase.mapping.value = State.Data(KeyMap())
+
+ useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any)
+ useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE)
+ useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE)
+ useCase.setParallelTriggerMode()
+
+ val trigger = useCase.mapping.value.dataOrNull()!!.trigger
+ assertThat(trigger.keys, hasSize(2))
+ assertThat(trigger.keys[0], instanceOf(KeyCodeTriggerKey::class.java))
+ assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java))
+ }
+
+ @Test
+ fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() =
+ runTest(testDispatcher) {
+ useCase.mapping.value = State.Data(KeyMap())
+
+ useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any)
+ useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE)
+ useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE)
+ useCase.setParallelTriggerMode()
+
+ val trigger = useCase.mapping.value.dataOrNull()!!.trigger
+ assertThat(trigger.keys, hasSize(2))
+ assertThat(trigger.keys[0], instanceOf(KeyCodeTriggerKey::class.java))
+ assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java))
+ }
+
+ @Test
+ fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() =
+ runTest(testDispatcher) {
+ useCase.mapping.value = State.Data(KeyMap())
+
+ useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any)
+ useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_UP, TriggerKeyDevice.Any)
+ useCase.setTriggerLongPress()
+
+ useCase.addAssistantTriggerKey(AssistantTriggerType.ANY)
+
+ val trigger = useCase.mapping.value.dataOrNull()!!.trigger
+ assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS)))
+ }
+
+ @Test
+ fun `Set click type to short press when adding assistant key to double press trigger key`() =
+ runTest(testDispatcher) {
+ useCase.mapping.value = State.Data(KeyMap())
+
+ useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any)
+ useCase.setTriggerDoublePress()
+ useCase.addAssistantTriggerKey(AssistantTriggerType.ANY)
+
+ val trigger = useCase.mapping.value.dataOrNull()!!.trigger
+ assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS)))
+ }
+
+ @Test
+ fun `Set click type to short press when adding assistant key to long press trigger key`() =
+ runTest(testDispatcher) {
+ useCase.mapping.value = State.Data(KeyMap())
+
+ useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, TriggerKeyDevice.Any)
+ useCase.setTriggerLongPress()
+ useCase.addAssistantTriggerKey(AssistantTriggerType.ANY)
+
+ val trigger = useCase.mapping.value.dataOrNull()!!.trigger
+ assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS)))
+ }
+
+ @Test
+ fun `Do not allow long press for parallel trigger with assistant key`() =
+ runTest(testDispatcher) {
+ val keyMap = KeyMap(
+ trigger = Trigger(
+ mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS),
+ keys = listOf(
+ triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN),
+ AssistantTriggerKey(
+ type = AssistantTriggerType.ANY,
+ clickType = ClickType.SHORT_PRESS,
+ ),
+ ),
+ ),
+ )
+
+ useCase.mapping.value = State.Data(keyMap)
+ useCase.setTriggerLongPress()
+
+ val trigger = useCase.mapping.value.dataOrNull()!!.trigger
+ assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS)))
+ }
/**
* Issue #753. If a modifier key is used as a trigger then it the
@@ -59,7 +156,7 @@ class ConfigKeyMapUseCaseTest {
*/
@Test
fun `when add modifier key trigger, enable do not remap option`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val modifierKeys = setOf(
KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_SHIFT_RIGHT,
@@ -79,12 +176,12 @@ class ConfigKeyMapUseCaseTest {
useCase.mapping.value = State.Data(KeyMap())
// WHEN
- useCase.addTriggerKey(modifierKeyCode, TriggerKeyDevice.Internal)
+ useCase.addKeyCodeTriggerKey(modifierKeyCode, TriggerKeyDevice.Internal)
// THEN
val trigger = useCase.mapping.value.dataOrNull()!!.trigger
- assertThat(trigger.keys[0].consumeKeyEvent, `is`(false))
+ assertThat(trigger.keys[0].consumeEvent, `is`(false))
}
}
@@ -93,17 +190,17 @@ class ConfigKeyMapUseCaseTest {
*/
@Test
fun `when add non-modifier key trigger, do ont enable do not remap option`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
useCase.mapping.value = State.Data(KeyMap())
// WHEN
- useCase.addTriggerKey(KeyEvent.KEYCODE_A, TriggerKeyDevice.Internal)
+ useCase.addKeyCodeTriggerKey(KeyEvent.KEYCODE_A, TriggerKeyDevice.Internal)
// THEN
val trigger = useCase.mapping.value.dataOrNull()!!.trigger
- assertThat(trigger.keys[0].consumeKeyEvent, `is`(true))
+ assertThat(trigger.keys[0].consumeEvent, `is`(true))
}
/**
@@ -112,7 +209,7 @@ class ConfigKeyMapUseCaseTest {
*/
@Test
fun `when add answer phone call action, then add phone ringing constraint`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
useCase.mapping.value = State.Data(KeyMap())
val action = ActionData.AnswerCall
@@ -131,7 +228,7 @@ class ConfigKeyMapUseCaseTest {
*/
@Test
fun `when add end phone call action, then add in phone call constraint`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
useCase.mapping.value = State.Data(KeyMap())
val action = ActionData.EndCall
@@ -149,7 +246,7 @@ class ConfigKeyMapUseCaseTest {
*/
@Test
fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val action = KeyMapAction(
data = ActionData.TapScreen(100, 100, null),
@@ -171,7 +268,7 @@ class ConfigKeyMapUseCaseTest {
@Test
fun `add modifier key event action, enable hold down option and disable repeat option`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
KeyEventUtils.MODIFIER_KEYCODES.forEach { keyCode ->
useCase.mapping.value = State.Data(KeyMap())
diff --git a/app/src/test/java/io/github/sds100/keymapper/KeyMapJsonMigrationTest.kt b/app/src/test/java/io/github/sds100/keymapper/KeyMapJsonMigrationTest.kt
index b05e653a77..3c028b19b1 100644
--- a/app/src/test/java/io/github/sds100/keymapper/KeyMapJsonMigrationTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/KeyMapJsonMigrationTest.kt
@@ -13,10 +13,9 @@ import io.github.sds100.keymapper.data.migration.Migration9To10
import io.github.sds100.keymapper.data.migration.MigrationUtils
import io.github.sds100.keymapper.util.JsonTestUtils
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -42,9 +41,8 @@ class KeyMapJsonMigrationTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private lateinit var parser: JsonParser
private lateinit var gson: Gson
@@ -111,7 +109,7 @@ class KeyMapJsonMigrationTest {
expectedData: JsonArray,
inputVersion: Int,
outputVersion: Int,
- ) = coroutineScope.runBlockingTest {
+ ) = runTest(testDispatcher) {
val migrations = listOf(
JsonMigration(9, 10) { json -> Migration9To10.migrateJson(json) },
JsonMigration(10, 11) { json -> Migration10To11.migrateJson(json) },
diff --git a/app/src/test/java/io/github/sds100/keymapper/LegacyFingerprintMapMigrationTest.kt b/app/src/test/java/io/github/sds100/keymapper/LegacyFingerprintMapMigrationTest.kt
index 48260c24b0..8c5f6813f6 100644
--- a/app/src/test/java/io/github/sds100/keymapper/LegacyFingerprintMapMigrationTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/LegacyFingerprintMapMigrationTest.kt
@@ -14,10 +14,9 @@ import io.github.sds100.keymapper.data.migration.fingerprintmaps.FingerprintMapM
import io.github.sds100.keymapper.data.migration.fingerprintmaps.FingerprintMapMigration1To2
import io.github.sds100.keymapper.util.JsonTestUtils
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -45,9 +44,9 @@ class LegacyFingerprintMapMigrationTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
private lateinit var parser: JsonParser
private lateinit var gson: Gson
@@ -97,7 +96,7 @@ class LegacyFingerprintMapMigrationTest {
expectedData: JsonArray,
inputVersion: Int,
outputVersion: Int,
- ) = coroutineScope.runBlockingTest {
+ ) = runTest(testDispatcher) {
val migrations = listOf(
JsonMigration(0, 1) { json -> FingerprintMapMigration0To1.migrate(json) },
JsonMigration(1, 2) { json -> FingerprintMapMigration1To2.migrate(json) },
diff --git a/app/src/test/java/io/github/sds100/keymapper/NotificationControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/NotificationControllerTest.kt
deleted file mode 100644
index ad298dcfac..0000000000
--- a/app/src/test/java/io/github/sds100/keymapper/NotificationControllerTest.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-package io.github.sds100.keymapper
-
-import androidx.core.app.NotificationCompat
-import io.github.sds100.keymapper.onboarding.FakeOnboardingUseCase
-import io.github.sds100.keymapper.system.accessibility.ServiceState
-import io.github.sds100.keymapper.system.notifications.ManageNotificationsUseCase
-import io.github.sds100.keymapper.system.notifications.NotificationController
-import io.github.sds100.keymapper.system.notifications.NotificationModel
-import io.github.sds100.keymapper.util.FlowUtils.toListWithTimeout
-import io.github.sds100.keymapper.util.ui.ResourceProvider
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.DelayController
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
-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.times
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
-import kotlin.coroutines.ContinuationInterceptor
-
-/**
- * Created by sds100 on 25/04/2021.
- */
-
-@ExperimentalCoroutinesApi
-@RunWith(MockitoJUnitRunner::class)
-class NotificationControllerTest {
-
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
-
- private lateinit var controller: NotificationController
- private lateinit var mockManageNotifications: ManageNotificationsUseCase
- private lateinit var mockResourceProvider: ResourceProvider
- private lateinit var fakeOnboarding: FakeOnboardingUseCase
-
- private lateinit var onActionClick: MutableSharedFlow
-
- @Before
- fun init() {
- onActionClick = MutableSharedFlow()
-
- mockManageNotifications = mock {
- on { onActionClick }.then { onActionClick }
- on { showToggleMappingsNotification }.then { flow { } }
- on { showImePickerNotification }.then { flow { } }
- }
-
- mockResourceProvider = mock()
- fakeOnboarding = FakeOnboardingUseCase()
-
- controller = NotificationController(
- coroutineScope,
- mockManageNotifications,
- pauseMappings = mock {
- on { isPaused }.then { flow {} }
- },
- showImePicker = mock(),
- controlAccessibilityService = mock {
- on { serviceState }.then { flow {} }
- },
- toggleCompatibleIme = mock {
- on { sufficientPermissions }.then { flow {} }
- },
- hideInputMethod = mock {
- on { onHiddenChange }.then { flow {} }
- },
- areFingerprintGesturesSupported = mock {
- on { isSupported }.then { flow {} }
- },
- onboardingUseCase = fakeOnboarding,
- resourceProvider = mockResourceProvider,
- dispatchers = TestDispatcherProvider(testDispatcher),
- )
- }
-
- @Test
- fun `click setup chosen devices notification, open app and approve`() =
- coroutineScope.runBlockingTest {
- // WHEN
-
- (coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()
- launch {
- onActionClick.emit(NotificationController.ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN)
- }
-
- // THEN
- assertThat(controller.openApp.toListWithTimeout().size, `is`(1))
- (coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()
-
- assertThat(fakeOnboarding.approvedSetupChosenDevicesAgainNotification, `is`(true))
- }
-
- @Test
- fun `show setup chosen devices notification`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val title = "title"
- val text = "text"
- whenever(mockResourceProvider.getString(R.string.notification_setup_chosen_devices_again_title)).then { title }
- whenever(mockResourceProvider.getString(R.string.notification_setup_chosen_devices_again_text)).then { text }
-
- // WHEN
- fakeOnboarding.showSetupChosenDevicesAgainNotification.value = true
-
- // THEN
- verify(
- mockResourceProvider,
- times(1),
- ).getString(R.string.notification_setup_chosen_devices_again_title)
-
- verify(
- mockResourceProvider,
- times(1),
- ).getString(R.string.notification_setup_chosen_devices_again_text)
-
- val expectedNotification = NotificationModel(
- id = NotificationController.ID_SETUP_CHOSEN_DEVICES_AGAIN,
- channel = NotificationController.CHANNEL_NEW_FEATURES,
- icon = R.drawable.ic_notification_settings,
- title = title,
- text = text,
- onClickActionId = NotificationController.ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN,
- showOnLockscreen = false,
- onGoing = false,
- priority = NotificationCompat.PRIORITY_LOW,
- actions = emptyList(),
- autoCancel = true,
- bigTextStyle = true,
- )
-
- verify(mockManageNotifications, times(1)).show(expectedNotification)
-
- // this should be called when the notification is clicked
- assertThat(fakeOnboarding.approvedSetupChosenDevicesAgainNotification, `is`(false))
- }
-}
diff --git a/app/src/test/java/io/github/sds100/keymapper/TestDispatcherProvider.kt b/app/src/test/java/io/github/sds100/keymapper/TestDispatcherProvider.kt
index 710e14e85b..f1384aba31 100644
--- a/app/src/test/java/io/github/sds100/keymapper/TestDispatcherProvider.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/TestDispatcherProvider.kt
@@ -1,14 +1,14 @@
package io.github.sds100.keymapper
import io.github.sds100.keymapper.util.DispatcherProvider
-import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.TestDispatcher
/**
* Created by sds100 on 01/05/2021.
*/
class TestDispatcherProvider(
- private val testDispatcher: TestCoroutineDispatcher,
+ private val testDispatcher: TestDispatcher,
) : DispatcherProvider {
override fun main() = testDispatcher
override fun default() = testDispatcher
diff --git a/app/src/test/java/io/github/sds100/keymapper/actions/GetActionErrorUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/actions/GetActionErrorUseCaseTest.kt
deleted file mode 100644
index d901f96a33..0000000000
--- a/app/src/test/java/io/github/sds100/keymapper/actions/GetActionErrorUseCaseTest.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package io.github.sds100.keymapper.actions
-
-import android.view.KeyEvent
-import io.github.sds100.keymapper.shizuku.ShizukuAdapter
-import io.github.sds100.keymapper.system.inputmethod.ImeInfo
-import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter
-import io.github.sds100.keymapper.system.permissions.PermissionAdapter
-import io.github.sds100.keymapper.util.Error
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.`is`
-import org.hamcrest.Matchers.nullValue
-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.whenever
-
-/**
- * Created by sds100 on 01/05/2021.
- */
-
-@ExperimentalCoroutinesApi
-@RunWith(MockitoJUnitRunner::class)
-class GetActionErrorUseCaseTest {
-
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
-
- private lateinit var useCase: GetActionErrorUseCaseImpl
-
- private lateinit var mockShizukuAdapter: ShizukuAdapter
- private lateinit var mockInputMethodAdapter: InputMethodAdapter
- private lateinit var mockPermissionAdapter: PermissionAdapter
-
- @Before
- fun init() {
- mockShizukuAdapter = mock()
- mockInputMethodAdapter = mock()
- mockPermissionAdapter = mock()
-
- useCase = GetActionErrorUseCaseImpl(
- packageManager = mock(),
- inputMethodAdapter = mockInputMethodAdapter,
- permissionAdapter = mockPermissionAdapter,
- systemFeatureAdapter = mock(),
- cameraAdapter = mock(),
- soundsManager = mock(),
- shizukuAdapter = mockShizukuAdapter,
- )
- }
-
- /**
- * #776
- */
- @Test
- fun `dont show Shizuku errors if a compatible ime is selected`() = coroutineScope.runBlockingTest {
- // GIVEN
- whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) }
- whenever(mockInputMethodAdapter.chosenIme).then {
- MutableStateFlow(
- ImeInfo(
- id = "ime_id",
- packageName = "io.github.sds100.keymapper.inputmethod.latin",
- label = "Key Mapper GUI Keyboard",
- isEnabled = true,
- isChosen = true,
- ),
- )
- }
-
- val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)
-
- // WHEN
- val error = useCase.getError(action)
-
- // THEN
- assertThat(error, nullValue())
- }
-
- /**
- * #776
- */
- @Test
- fun `show Shizuku errors if a compatible ime is not selected and Shizuku is installed`() = coroutineScope.runBlockingTest {
- // GIVEN
- whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) }
- whenever(mockShizukuAdapter.isStarted).then { MutableStateFlow(false) }
-
- whenever(mockInputMethodAdapter.chosenIme).then {
- MutableStateFlow(
- ImeInfo(
- id = "ime_id",
- packageName = "io.gboard",
- label = "Gboard",
- isEnabled = true,
- isChosen = true,
- ),
- )
- }
-
- val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)
- // WHEN
- val error = useCase.getError(action)
-
- // THEN
- assertThat(error, `is`(Error.ShizukuNotStarted))
- }
-}
diff --git a/app/src/test/java/io/github/sds100/keymapper/actions/GetActionFailedUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/actions/GetActionFailedUseCaseTest.kt
new file mode 100644
index 0000000000..72cf56fb62
--- /dev/null
+++ b/app/src/test/java/io/github/sds100/keymapper/actions/GetActionFailedUseCaseTest.kt
@@ -0,0 +1,116 @@
+package io.github.sds100.keymapper.actions
+
+import android.view.KeyEvent
+import io.github.sds100.keymapper.shizuku.ShizukuAdapter
+import io.github.sds100.keymapper.system.inputmethod.ImeInfo
+import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter
+import io.github.sds100.keymapper.system.permissions.PermissionAdapter
+import io.github.sds100.keymapper.util.Error
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.nullValue
+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.whenever
+
+/**
+ * Created by sds100 on 01/05/2021.
+ */
+
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner::class)
+class GetActionFailedUseCaseTest {
+
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private lateinit var useCase: GetActionErrorUseCaseImpl
+
+ private lateinit var mockShizukuAdapter: ShizukuAdapter
+ private lateinit var mockInputMethodAdapter: InputMethodAdapter
+ private lateinit var mockPermissionAdapter: PermissionAdapter
+
+ @Before
+ fun init() {
+ mockShizukuAdapter = mock()
+ mockInputMethodAdapter = mock()
+ mockPermissionAdapter = mock()
+
+ useCase = GetActionErrorUseCaseImpl(
+ packageManager = mock(),
+ inputMethodAdapter = mockInputMethodAdapter,
+ permissionAdapter = mockPermissionAdapter,
+ systemFeatureAdapter = mock(),
+ cameraAdapter = mock(),
+ soundsManager = mock(),
+ shizukuAdapter = mockShizukuAdapter,
+ )
+ }
+
+ /**
+ * #776
+ */
+ @Test
+ fun `don't show Shizuku errors if a compatible ime is selected`() =
+ testScope.runTest {
+ // GIVEN
+ whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) }
+ whenever(mockInputMethodAdapter.chosenIme).then {
+ MutableStateFlow(
+ ImeInfo(
+ id = "ime_id",
+ packageName = "io.github.sds100.keymapper.inputmethod.latin",
+ label = "Key Mapper GUI Keyboard",
+ isEnabled = true,
+ isChosen = true,
+ ),
+ )
+ }
+
+ val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)
+
+ // WHEN
+ val error = useCase.getError(action)
+
+ // THEN
+ assertThat(error, nullValue())
+ }
+
+ /**
+ * #776
+ */
+ @Test
+ fun `show Shizuku errors if a compatible ime is not selected and Shizuku is installed`() =
+ testScope.runTest {
+ // GIVEN
+ whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) }
+ whenever(mockShizukuAdapter.isStarted).then { MutableStateFlow(false) }
+
+ whenever(mockInputMethodAdapter.chosenIme).then {
+ MutableStateFlow(
+ ImeInfo(
+ id = "ime_id",
+ packageName = "io.gboard",
+ label = "Gboard",
+ isEnabled = true,
+ isChosen = true,
+ ),
+ )
+ }
+
+ val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)
+ // WHEN
+ val error = useCase.getError(action)
+
+ // THEN
+ assertThat(error, `is`(Error.ShizukuNotStarted))
+ }
+}
diff --git a/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt
index 6127261d01..483d51af3c 100644
--- a/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt
@@ -12,10 +12,9 @@ import io.github.sds100.keymapper.util.InputEventType
import io.github.sds100.keymapper.util.State
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+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
@@ -37,9 +36,8 @@ import org.mockito.kotlin.whenever
@RunWith(MockitoJUnitRunner::class)
class PerformActionsUseCaseTest {
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private lateinit var useCase: PerformActionsUseCaseImpl
private lateinit var mockKeyMapperImeMessenger: KeyMapperImeMessenger
@@ -55,7 +53,7 @@ class PerformActionsUseCaseTest {
mockToastAdapter = mock()
useCase = PerformActionsUseCaseImpl(
- coroutineScope,
+ testScope,
accessibilityService = mockAccessibilityService,
inputMethodAdapter = mock(),
fileAdapter = mock(),
@@ -95,7 +93,7 @@ class PerformActionsUseCaseTest {
*/
@Test
fun `dont show accessibility service not found error for open menu action`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = ActionData.OpenMenu
@@ -118,7 +116,7 @@ class PerformActionsUseCaseTest {
*/
@Test
fun `set the device id of key event actions to a connected game controller if is a game pad key code`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val fakeGamePad = InputDeviceInfo(
descriptor = "game_pad",
@@ -156,7 +154,7 @@ class PerformActionsUseCaseTest {
*/
@Test
fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList())
@@ -186,7 +184,7 @@ class PerformActionsUseCaseTest {
*/
@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`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val fakeGamePad = InputDeviceInfo(
descriptor = "game_pad",
@@ -236,7 +234,7 @@ class PerformActionsUseCaseTest {
*/
@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`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val descriptor = "fake_device_descriptor"
@@ -271,9 +269,7 @@ class PerformActionsUseCaseTest {
)
// none of the devices support the key code
- fakeDevicesAdapter.deviceHasKey = { id, keyCode ->
- false
- }
+ fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false }
// WHEN
useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0)
@@ -293,7 +289,7 @@ class PerformActionsUseCaseTest {
@Test
fun `perform key event action with no device name, ensure action is still performed with correct device id`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val descriptor = "fake_device_descriptor"
diff --git a/app/src/test/java/io/github/sds100/keymapper/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt
index b223aed38d..e28c7f0db1 100644
--- a/app/src/test/java/io/github/sds100/keymapper/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt
@@ -7,12 +7,11 @@ import io.github.sds100.keymapper.system.devices.InputDeviceInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.createTestCoroutineScope
import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
@@ -36,9 +35,9 @@ class ConfigKeyServiceEventActionViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
private lateinit var viewModel: ConfigKeyEventActionViewModel
private lateinit var mockUseCase: ConfigKeyEventUseCase
@@ -63,13 +62,12 @@ class ConfigKeyServiceEventActionViewModelTest {
@After
fun tearDown() {
- testDispatcher.cleanupTestCoroutines()
Dispatchers.resetMain()
}
@Test
fun `multiple input devices with same descriptor but a different name, choose a device, ensure device with correct name is chosen`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val fakeDevice1 = InputDeviceInfo(
descriptor = "bla",
@@ -92,7 +90,7 @@ class ConfigKeyServiceEventActionViewModelTest {
// THEN
viewModel.chooseDevice(0)
- coroutineScope.advanceUntilIdle()
+ testScope.advanceUntilIdle()
assertThat(viewModel.uiState.value.chosenDeviceName, `is`(fakeDevice1.name))
diff --git a/app/src/test/java/io/github/sds100/keymapper/data/repositories/FingerprintMapRepositoryTest.kt b/app/src/test/java/io/github/sds100/keymapper/data/repositories/FingerprintMapRepositoryTest.kt
index 5da322de5f..2a69ebf06e 100644
--- a/app/src/test/java/io/github/sds100/keymapper/data/repositories/FingerprintMapRepositoryTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/data/repositories/FingerprintMapRepositoryTest.kt
@@ -11,10 +11,9 @@ import io.github.sds100.keymapper.util.State
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+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
@@ -43,9 +42,8 @@ class FingerprintMapRepositoryTest {
)
}
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private val dispatchers = TestDispatcherProvider(testDispatcher)
private lateinit var repository: RoomFingerprintMapRepository
@@ -65,7 +63,7 @@ class FingerprintMapRepositoryTest {
repository = RoomFingerprintMapRepository(
mockDao,
- coroutineScope,
+ testScope,
devicesAdapter,
dispatchers = dispatchers,
)
@@ -73,8 +71,8 @@ class FingerprintMapRepositoryTest {
@Test
fun `only swipe down fingerprint map in database, insert 3 blank fingerprint maps for the other fingerprint maps`() =
- coroutineScope.runBlockingTest {
- repository.fingerprintMapList.launchIn(coroutineScope)
+ runTest(testDispatcher) {
+ repository.fingerprintMapList.launchIn(testScope)
fingerprintMaps.emit(listOf(FingerprintMapEntity(id = FingerprintMapEntity.ID_SWIPE_DOWN)))
@@ -87,8 +85,8 @@ class FingerprintMapRepositoryTest {
@Test
fun `no fingerprint maps in database, insert 4 blank fingerprint maps`() =
- coroutineScope.runBlockingTest {
- repository.fingerprintMapList.launchIn(coroutineScope)
+ runTest(testDispatcher) {
+ repository.fingerprintMapList.launchIn(testScope)
fingerprintMaps.emit(emptyList())
@@ -102,7 +100,7 @@ class FingerprintMapRepositoryTest {
@Test
fun `fingerprint map with key event action from device and proper device name extra, do not update action device name`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = ActionEntity(
type = ActionEntity.Type.KEY_EVENT,
@@ -126,7 +124,7 @@ class FingerprintMapRepositoryTest {
@Test
fun `fingerprint map with key event action from device and blank device name extra, if device for action is disconnected, do not update action device name`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = ActionEntity(
type = ActionEntity.Type.KEY_EVENT,
@@ -150,7 +148,7 @@ class FingerprintMapRepositoryTest {
@Test
fun `fingerprint map with key event action from device and blank device name extra, if device for action is connected, update action device name`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = ActionEntity(
type = ActionEntity.Type.KEY_EVENT,
@@ -192,7 +190,7 @@ class FingerprintMapRepositoryTest {
@Test
fun `fingerprint map with key event action from device and no device name extra, if device for action is connected, update action device name`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = ActionEntity(
type = ActionEntity.Type.KEY_EVENT,
@@ -234,7 +232,7 @@ class FingerprintMapRepositoryTest {
@Test
fun `fingerprint map with key event action from device and no device name extra, if device for action is disconnected, update action device name`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = ActionEntity(
type = ActionEntity.Type.KEY_EVENT,
diff --git a/app/src/test/java/io/github/sds100/keymapper/data/repositories/KeyMapRepositoryTest.kt b/app/src/test/java/io/github/sds100/keymapper/data/repositories/KeyMapRepositoryTest.kt
index 3270b5b785..08c4660ad2 100644
--- a/app/src/test/java/io/github/sds100/keymapper/data/repositories/KeyMapRepositoryTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/data/repositories/KeyMapRepositoryTest.kt
@@ -2,30 +2,22 @@ package io.github.sds100.keymapper.data.repositories
import io.github.sds100.keymapper.TestDispatcherProvider
import io.github.sds100.keymapper.data.db.dao.KeyMapDao
-import io.github.sds100.keymapper.data.entities.ActionEntity
-import io.github.sds100.keymapper.data.entities.Extra
import io.github.sds100.keymapper.data.entities.KeyMapEntity
-import io.github.sds100.keymapper.data.entities.TriggerEntity
import io.github.sds100.keymapper.system.devices.FakeDevicesAdapter
import io.github.sds100.keymapper.system.devices.InputDeviceInfo
-import io.github.sds100.keymapper.util.State
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+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.junit.MockitoJUnitRunner
-import org.mockito.kotlin.any
import org.mockito.kotlin.anyVararg
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
import org.mockito.kotlin.times
-import org.mockito.kotlin.verify
/**
* Created by sds100 on 01/05/2021.
@@ -45,9 +37,8 @@ class KeyMapRepositoryTest {
)
}
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private lateinit var repository: RoomKeyMapRepository
private lateinit var devicesAdapter: FakeDevicesAdapter
@@ -66,8 +57,7 @@ class KeyMapRepositoryTest {
repository = RoomKeyMapRepository(
mockDao,
- devicesAdapter,
- coroutineScope,
+ testScope,
dispatchers = TestDispatcherProvider(testDispatcher),
)
}
@@ -77,7 +67,7 @@ class KeyMapRepositoryTest {
*/
@Test
fun `if modifying a huge number of key maps then split job into batches`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val keyMapList = sequence {
repeat(991) {
@@ -109,216 +99,4 @@ class KeyMapRepositoryTest {
verify(mockDao, times(5)).update(anyVararg())
}
}
-
- @Test
- fun `key map with key event action from device and proper device name extra, do not update action device name`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val action = ActionEntity(
- type = ActionEntity.Type.KEY_EVENT,
- data = "1",
- extras = listOf(
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR, FAKE_KEYBOARD.descriptor),
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME, FAKE_KEYBOARD.name),
- ),
- )
-
- val keyMap = KeyMapEntity(id = 0, actionList = listOf(action))
-
- devicesAdapter.connectedInputDevices.value = State.Data(listOf(FAKE_KEYBOARD))
-
- // WHEN
- keyMaps.emit(listOf(keyMap))
-
- // THEN
- verify(mockDao, never()).update(any())
- }
-
- @Test
- fun `key map with key event action from device and blank device name extra, if device for action is disconnected, do not update action device name`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val action = ActionEntity(
- type = ActionEntity.Type.KEY_EVENT,
- data = "1",
- extras = listOf(
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR, FAKE_KEYBOARD.descriptor),
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME, ""),
- ),
- )
-
- val keyMap = KeyMapEntity(id = 0, actionList = listOf(action))
-
- devicesAdapter.connectedInputDevices.value = State.Data(emptyList())
-
- // WHEN
- keyMaps.emit(listOf(keyMap))
-
- // THEN
- verify(mockDao, never()).update(any())
- }
-
- @Test
- fun `key map with key event action from device and blank device name extra, if device for action is connected, update action device name`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val action = ActionEntity(
- type = ActionEntity.Type.KEY_EVENT,
- data = "1",
- extras = listOf(
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR, FAKE_KEYBOARD.descriptor),
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME, ""),
- ),
- )
-
- val keyMap = KeyMapEntity(id = 0, actionList = listOf(action))
-
- devicesAdapter.connectedInputDevices.value = State.Data(
- listOf(FAKE_KEYBOARD),
- )
-
- // WHEN
- keyMaps.emit(listOf(keyMap))
-
- val expectedAction = action.copy(
- extras = listOf(
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR, FAKE_KEYBOARD.descriptor),
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME, FAKE_KEYBOARD.name),
- ),
- )
-
- // THEN
- verify(mockDao, times(1)).update(
- keyMap.copy(actionList = listOf(expectedAction)),
- )
- }
-
- @Test
- fun `key map with key event action from device and no device name extra, if device for action is connected, update action device name`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val action = ActionEntity(
- type = ActionEntity.Type.KEY_EVENT,
- data = "1",
- extra = Extra(
- ActionEntity.EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR,
- FAKE_KEYBOARD.descriptor,
- ),
- )
-
- val keyMap = KeyMapEntity(id = 0, actionList = listOf(action))
-
- devicesAdapter.connectedInputDevices.value = State.Data(
- listOf(FAKE_KEYBOARD),
- )
-
- // WHEN
- keyMaps.emit(listOf(keyMap))
-
- val expectedAction = action.copy(
- extras = listOf(
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR, FAKE_KEYBOARD.descriptor),
- Extra(ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME, FAKE_KEYBOARD.name),
- ),
- )
-
- // THEN
- verify(mockDao, times(1)).update(
- keyMap.copy(actionList = listOf(expectedAction)),
- )
- }
-
- @Test
- fun `key map with key event action from device and no device name extra, if device for action is disconnected, update action device name`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val action = ActionEntity(
- type = ActionEntity.Type.KEY_EVENT,
- data = "1",
- extra = Extra(
- ActionEntity.EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR,
- FAKE_KEYBOARD.descriptor,
- ),
- )
-
- val keyMap = KeyMapEntity(id = 0, actionList = listOf(action))
-
- devicesAdapter.connectedInputDevices.value = State.Data(emptyList())
-
- // WHEN
- keyMaps.emit(listOf(keyMap))
-
- // THEN
- verify(mockDao, never()).update(any())
- }
-
- @Test
- fun `key map with device name for trigger key, if device for trigger key is connected, do not update trigger key device name`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val triggerKey = TriggerEntity.KeyEntity(
- keyCode = 1,
- deviceId = FAKE_KEYBOARD.descriptor,
- deviceName = FAKE_KEYBOARD.name,
- )
-
- val keyMap = KeyMapEntity(id = 0, trigger = TriggerEntity(keys = listOf(triggerKey)))
-
- devicesAdapter.connectedInputDevices.value = State.Data(
- listOf(FAKE_KEYBOARD),
- )
-
- // WHEN
- keyMaps.emit(listOf(keyMap))
-
- // THEN
- verify(mockDao, never()).update(any())
- }
-
- @Test
- fun `key map with device name for trigger key, if device for trigger key is disconnected, do not update trigger key device name`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val triggerKey = TriggerEntity.KeyEntity(
- keyCode = 1,
- deviceId = FAKE_KEYBOARD.descriptor,
- deviceName = FAKE_KEYBOARD.name,
- )
-
- val keyMap = KeyMapEntity(id = 0, trigger = TriggerEntity(keys = listOf(triggerKey)))
-
- devicesAdapter.connectedInputDevices.value = State.Data(emptyList())
-
- // WHEN
- keyMaps.emit(listOf(keyMap))
-
- // THEN
- verify(mockDao, never()).update(any())
- }
-
- @Test
- fun `key map with no device name for trigger key, if device for trigger key is connected, update trigger key device name`() =
- coroutineScope.runBlockingTest {
- // GIVEN
- val triggerKey = TriggerEntity.KeyEntity(
- keyCode = 1,
- deviceId = FAKE_KEYBOARD.descriptor,
- deviceName = "",
- )
-
- val keyMap = KeyMapEntity(id = 0, trigger = TriggerEntity(keys = listOf(triggerKey)))
-
- devicesAdapter.connectedInputDevices.value = State.Data(
- listOf(FAKE_KEYBOARD),
- )
-
- // WHEN
- keyMaps.emit(listOf(keyMap))
-
- // THEN
- val expectedTriggerKey = triggerKey.copy(deviceName = FAKE_KEYBOARD.name)
-
- verify(mockDao, times(1))
- .update(keyMap.copy(trigger = TriggerEntity(listOf(expectedTriggerKey))))
- }
}
diff --git a/app/src/test/java/io/github/sds100/keymapper/home/HomeMenuViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/home/HomeMenuViewModelTest.kt
deleted file mode 100644
index 002a4674d5..0000000000
--- a/app/src/test/java/io/github/sds100/keymapper/home/HomeMenuViewModelTest.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package io.github.sds100.keymapper.home
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import io.github.sds100.keymapper.R
-import io.github.sds100.keymapper.util.ui.FakeResourceProvider
-import io.github.sds100.keymapper.util.ui.PopupUi
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
-import kotlinx.coroutines.withTimeout
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.`is`
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.mockito.kotlin.mock
-
-/**
- * Created by sds100 on 29/04/2022.
- */
-@ExperimentalCoroutinesApi
-class HomeMenuViewModelTest {
-
- @get:Rule
- var instantExecutorRule = InstantTaskExecutorRule()
- private val testDispatcher = TestCoroutineDispatcher()
- private val testCoroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
-
- private lateinit var fakeResourceProvider: FakeResourceProvider
- private lateinit var viewModel: HomeMenuViewModel
-
- @Before
- fun setUp() {
- fakeResourceProvider = FakeResourceProvider()
- viewModel = HomeMenuViewModel(
- testCoroutineScope,
- alertsUseCase = mock(),
- pauseMappings = mock(),
- showImePicker = mock(),
- fakeResourceProvider,
- )
- }
-
- @Test
- fun onCreateDocumentActivityNotFound() = runBlockingTest {
- // given
- fakeResourceProvider.stringResourceMap[R.string.dialog_message_no_app_found_to_create_file] = "message"
- fakeResourceProvider.stringResourceMap[R.string.pos_ok] = "ok"
-
- // when
- viewModel.onCreateBackupFileActivityNotFound()
-
- // then
- withTimeout(1000) {
- val popupEvent = viewModel.showPopup.first()
- assertThat(popupEvent.ui, `is`(PopupUi.Dialog(message = "message", positiveButtonText = "ok")))
- }
- }
-
- @Test
- fun onGetContentActivityNotFound() = runBlockingTest {
- // given
- fakeResourceProvider.stringResourceMap[R.string.dialog_message_no_app_found_to_choose_a_file] = "message"
- fakeResourceProvider.stringResourceMap[R.string.pos_ok] = "ok"
-
- // when
- viewModel.onChooseRestoreFileActivityNotFound()
-
- // then
- withTimeout(1000) {
- val popupEvent = viewModel.showPopup.first()
- assertThat(popupEvent.ui, `is`(PopupUi.Dialog(message = "message", positiveButtonText = "ok")))
- }
- }
-}
diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/SimpleMappingControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/SimpleMappingControllerTest.kt
index 4e035540c8..1ae33aaaef 100644
--- a/app/src/test/java/io/github/sds100/keymapper/mappings/SimpleMappingControllerTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/mappings/SimpleMappingControllerTest.kt
@@ -9,12 +9,10 @@ import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase
import junitparams.JUnitParamsRunner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.After
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -43,9 +41,8 @@ class SimpleMappingControllerTest {
private const val HOLD_DOWN_DURATION = 1000L
}
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private lateinit var controller: SimpleMappingController
private lateinit var detectMappingUseCase: DetectMappingUseCase
@@ -90,24 +87,19 @@ class SimpleMappingControllerTest {
}
controller = FakeSimpleMappingController(
- coroutineScope,
+ testScope,
detectMappingUseCase,
performActionsUseCase,
detectConstraintsUseCase,
)
}
- @After
- fun tearDown() {
- coroutineScope.cleanupTestCoroutines()
- }
-
/**
* #663
*/
@Test
fun `action with repeat until limit reached shouldn't stop repeating when trigger is detected again`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = FakeAction(
data = ActionData.InputKeyEvent(1),
@@ -131,7 +123,7 @@ class SimpleMappingControllerTest {
*/
@Test
fun `when triggering action that repeats until limit reached, then stop repeating when the limit has been reached`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = FakeAction(
data = ActionData.InputKeyEvent(keyCode = 1),
@@ -153,7 +145,7 @@ class SimpleMappingControllerTest {
*/
@Test
fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when the trigger has been pressed again`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = FakeAction(
data = ActionData.InputKeyEvent(keyCode = 1),
@@ -180,7 +172,7 @@ class SimpleMappingControllerTest {
*/
@Test
fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when limit reached and trigger hasn't been pressed again`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = FakeAction(
data = ActionData.InputKeyEvent(keyCode = 1),
diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigTriggerViewModelTest.kt
similarity index 80%
rename from app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModelTest.kt
rename to app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigTriggerViewModelTest.kt
index 6c3be50b09..dc9e2bf70d 100644
--- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModelTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigTriggerViewModelTest.kt
@@ -2,6 +2,7 @@ package io.github.sds100.keymapper.mappings.keymaps
import android.view.KeyEvent
import io.github.sds100.keymapper.R
+import io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel
import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerState
import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerUseCase
import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordedKey
@@ -16,13 +17,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
-import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,12 +35,12 @@ import org.mockito.kotlin.mock
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
-class ConfigKeyMapTriggerViewModelTest {
+class ConfigTriggerViewModelTest {
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
- private lateinit var viewModel: ConfigKeyMapTriggerViewModel
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private lateinit var viewModel: ConfigTriggerViewModel
private lateinit var mockConfigKeyMapUseCase: ConfigKeyMapUseCase
private lateinit var mockRecordTrigger: RecordTriggerUseCase
private lateinit var fakeOnboarding: FakeOnboardingUseCase
@@ -68,8 +67,8 @@ class ConfigKeyMapTriggerViewModelTest {
fakeResourceProvider = FakeResourceProvider()
- viewModel = ConfigKeyMapTriggerViewModel(
- coroutineScope,
+ viewModel = ConfigTriggerViewModel(
+ testScope,
fakeOnboarding,
mockConfigKeyMapUseCase,
mockRecordTrigger,
@@ -80,20 +79,16 @@ class ConfigKeyMapTriggerViewModelTest {
onBlocking { getTriggerErrors(any()) }.thenReturn(emptyList())
},
fakeResourceProvider,
+ purchasingManager = mock(),
)
}
- @After
- fun tearDown() {
- coroutineScope.cleanupTestCoroutines()
- }
-
/**
* issue #602
*/
@Test
fun `when create back button trigger key then prompt the user to disable screen pinning`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
fakeResourceProvider.stringResourceMap[R.string.dialog_message_screen_pinning_warning] =
"bla"
diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyCodeTriggerKeyMapFromOtherAppsControllerTest.kt
similarity index 70%
rename from app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt
rename to app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyCodeTriggerKeyMapFromOtherAppsControllerTest.kt
index 7bee039558..9f78768655 100644
--- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyCodeTriggerKeyMapFromOtherAppsControllerTest.kt
@@ -6,17 +6,16 @@ import io.github.sds100.keymapper.actions.RepeatMode
import io.github.sds100.keymapper.constraints.ConstraintSnapshotImpl
import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase
import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTrigger
+import io.github.sds100.keymapper.mappings.keymaps.detection.TriggerKeyMapFromOtherAppsController
+import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger
import junitparams.JUnitParamsRunner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.After
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -31,7 +30,7 @@ import org.mockito.kotlin.verify
@ExperimentalCoroutinesApi
@RunWith(JUnitParamsRunner::class)
-class TriggerKeyMapFromOtherAppsControllerTest {
+class KeyCodeTriggerKeyMapFromOtherAppsControllerTest {
companion object {
private const val LONG_PRESS_DELAY = 500L
@@ -44,9 +43,8 @@ class TriggerKeyMapFromOtherAppsControllerTest {
private const val HOLD_DOWN_DURATION = 1000L
}
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private lateinit var controller: TriggerKeyMapFromOtherAppsController
private lateinit var detectKeyMapsUseCase: DetectKeyMapsUseCase
@@ -96,42 +94,41 @@ class TriggerKeyMapFromOtherAppsControllerTest {
}
controller = TriggerKeyMapFromOtherAppsController(
- coroutineScope,
+ testScope,
detectKeyMapsUseCase,
performActionsUseCase,
detectConstraintsUseCase,
)
}
- @After
- fun tearDown() {
- coroutineScope.cleanupTestCoroutines()
- }
-
/**
* #707
*/
@Test
- fun `Key map with repeat option, don't repeat when triggered if repeat until released`() = coroutineScope.runBlockingTest {
- // GIVEN
- val action =
- KeyMapAction(
- data = ActionData.InputKeyEvent(keyCode = 1),
- repeat = true,
- repeatMode = RepeatMode.TRIGGER_RELEASED,
+ fun `Key map with repeat option, don't repeat when triggered if repeat until released`() =
+ runTest(testDispatcher) {
+ // GIVEN
+ val action =
+ KeyMapAction(
+ data = ActionData.InputKeyEvent(keyCode = 1),
+ repeat = true,
+ repeatMode = RepeatMode.TRIGGER_RELEASED,
+ )
+ val keyMap = KeyMap(
+ actionList = listOf(action),
+ trigger = Trigger(triggerFromOtherApps = true),
)
- val keyMap = KeyMap(actionList = listOf(action), trigger = KeyMapTrigger(triggerFromOtherApps = true))
- keyMapListFlow.value = listOf(keyMap)
+ keyMapListFlow.value = listOf(keyMap)
- advanceUntilIdle()
+ advanceUntilIdle()
- // WHEN
- controller.onDetected(keyMap.uid)
- delay(500)
- controller.reset() // stop any repeating that might be happening
- advanceUntilIdle()
+ // WHEN
+ controller.onDetected(keyMap.uid)
+ delay(500)
+ controller.reset() // stop any repeating that might be happening
+ advanceUntilIdle()
- // THEN
- verify(performActionsUseCase, times(1)).perform(action.data)
- }
+ // THEN
+ verify(performActionsUseCase, times(1)).perform(action.data)
+ }
}
diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt
index 547ae25749..ff930a4603 100644
--- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt
@@ -12,7 +12,8 @@ import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase
import io.github.sds100.keymapper.mappings.ClickType
import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase
import io.github.sds100.keymapper.mappings.keymaps.detection.KeyMapController
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTrigger
+import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode
@@ -30,15 +31,13 @@ import junitparams.naming.TestCaseName
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.createTestCoroutineScope
import kotlinx.coroutines.test.currentTime
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
-import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -107,9 +106,8 @@ class KeyMapControllerTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
@Before
fun init() {
@@ -139,7 +137,7 @@ class KeyMapControllerTest {
}
}
- whenever(detectKeyMapsUseCase.currentTime).thenAnswer { coroutineScope.currentTime }
+ whenever(detectKeyMapsUseCase.currentTime).thenAnswer { testScope.currentTime }
performActionsUseCase = mock {
MutableStateFlow(REPEAT_DELAY).apply {
@@ -160,21 +158,96 @@ class KeyMapControllerTest {
}
controller = KeyMapController(
- coroutineScope,
+ testScope,
detectKeyMapsUseCase,
performActionsUseCase,
detectConstraintsUseCase,
)
}
- @After
- fun tearDown() {
- coroutineScope.cleanupTestCoroutines()
- }
+ /**
+ * #1271 but with long press trigger instead of double press.
+ */
+ @Test
+ fun `Trigger short press key map if constraints allow it and a long press key map to the same button is not allowed`() =
+ runTest(testDispatcher) {
+ val shortPressTrigger = singleKeyTrigger(
+ triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN),
+ )
+ val shortPressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOn))
+
+ val longPressTrigger = singleKeyTrigger(
+ triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS),
+ )
+ val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff))
+
+ keyMapListFlow.value = listOf(
+ KeyMap(
+ 0,
+ trigger = shortPressTrigger,
+ actionList = listOf(TEST_ACTION),
+ constraintState = shortPressConstraints,
+ ),
+ KeyMap(
+ 1,
+ trigger = longPressTrigger,
+ actionList = listOf(TEST_ACTION_2),
+ constraintState = doublePressConstraints,
+ ),
+ )
+
+ // Only the short press trigger is allowed.
+ mockConstraintSnapshot { constraint -> constraint == Constraint.WifiOn }
+
+ mockTriggerKeyInput(shortPressTrigger.keys.first())
+
+ verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data)
+ }
+
+ /**
+ * #1271
+ */
+ @Test
+ fun `ignore double press key maps overlapping short press key maps if the constraints aren't satisfied`() =
+ runTest(testDispatcher) {
+ val shortPressTrigger = singleKeyTrigger(
+ triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN),
+ )
+ val shortPressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOn))
+
+ val doublePressTrigger = singleKeyTrigger(
+ triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS),
+ )
+ val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff))
+
+ keyMapListFlow.value = listOf(
+ KeyMap(
+ 0,
+ trigger = shortPressTrigger,
+ actionList = listOf(TEST_ACTION),
+ constraintState = shortPressConstraints,
+ ),
+ KeyMap(
+ 1,
+ trigger = doublePressTrigger,
+ actionList = listOf(TEST_ACTION_2),
+ constraintState = doublePressConstraints,
+ ),
+ )
+
+ // Only the short press trigger is allowed.
+ mockConstraintSnapshot { constraint -> constraint == Constraint.WifiOn }
+
+ mockTriggerKeyInput(shortPressTrigger.keys.first())
+
+ verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data)
+ verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data)
+ }
@Test
fun `Don't imitate button if 1 long press trigger is successful and another with a longer delay fails`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val longerTrigger =
@@ -250,7 +323,7 @@ class KeyMapControllerTest {
*/
@Test
fun `Long press trigger shouldn't be triggered if the constraints are changed by the actions`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val actionData = ActionData.Flashlight.Toggle(CameraLens.BACK)
@@ -303,10 +376,10 @@ class KeyMapControllerTest {
*/
@Test
fun `multiple key maps with the same long press trigger but different long press delays should all work`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val keyMap1 = KeyMap(
- trigger = KeyMapTrigger(
+ trigger = Trigger(
keys = listOf(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)),
longPressDelay = 500,
),
@@ -314,7 +387,7 @@ class KeyMapControllerTest {
)
val keyMap2 = KeyMap(
- trigger = KeyMapTrigger(
+ trigger = Trigger(
keys = listOf(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)),
longPressDelay = 1000,
),
@@ -362,7 +435,7 @@ class KeyMapControllerTest {
*/
@Test
fun `don't consume down and up event if no valid actions to perform`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
val actionList = listOf(KeyMapAction(data = ActionData.InputKeyEvent(2)))
@@ -387,7 +460,7 @@ class KeyMapControllerTest {
* #689
*/
@Test
- fun `perform all actions once when key map is triggered`() = coroutineScope.runBlockingTest {
+ fun `perform all actions once when key map is triggered`() = runTest(testDispatcher) {
// GIVEN
val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
@@ -415,7 +488,7 @@ class KeyMapControllerTest {
*/
@Test
fun `action with repeat until limit reached shouldn't stop repeating when trigger is released`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
@@ -445,7 +518,7 @@ class KeyMapControllerTest {
@Test
fun `key map with multiple actions and delay in between, perform all actions even when trigger is released`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
@@ -487,7 +560,7 @@ class KeyMapControllerTest {
@Test
fun `multiple key maps with same trigger, perform both key maps`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
@@ -525,7 +598,7 @@ class KeyMapControllerTest {
*/
@Test
fun `when triggering action that repeats until limit reached, then stop repeating when the limit has been reached and not when the trigger is released`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = KeyMapAction(
data = ActionData.InputKeyEvent(keyCode = 1),
@@ -554,7 +627,7 @@ class KeyMapControllerTest {
*/
@Test
fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when the trigger has been pressed again`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = KeyMapAction(
data = ActionData.InputKeyEvent(keyCode = 1),
@@ -589,7 +662,7 @@ class KeyMapControllerTest {
*/
@Test
fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when limit reached and trigger hasn't been pressed again`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = KeyMapAction(
data = ActionData.InputKeyEvent(keyCode = 1),
@@ -623,7 +696,7 @@ class KeyMapControllerTest {
*/
@Test
fun `when triggering action that repeats until released with repeat limit, then stop repeating when the trigger has been released`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = KeyMapAction(
data = ActionData.InputKeyEvent(keyCode = 1),
@@ -653,7 +726,7 @@ class KeyMapControllerTest {
*/
@Test
fun `when triggering action that repeats until released with repeat limit, then stop repeating when the limit has been reached and the action is still being held down`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = KeyMapAction(
data = ActionData.InputKeyEvent(keyCode = 1),
@@ -682,7 +755,7 @@ class KeyMapControllerTest {
* issue #653
*/
@Test
- fun `overlapping triggers 3`() = coroutineScope.runBlockingTest {
+ fun `overlapping triggers 3`() = runTest(testDispatcher) {
// GIVEN
val keyMaps = listOf(
KeyMap(
@@ -750,7 +823,7 @@ class KeyMapControllerTest {
* issue #653
*/
@Test
- fun `overlapping triggers 2`() = coroutineScope.runBlockingTest {
+ fun `overlapping triggers 2`() = runTest(testDispatcher) {
// GIVEN
val keyMaps = listOf(
KeyMap(
@@ -811,7 +884,7 @@ class KeyMapControllerTest {
* issue #653
*/
@Test
- fun `overlapping triggers 1`() = coroutineScope.runBlockingTest {
+ fun `overlapping triggers 1`() = runTest(testDispatcher) {
// GIVEN
val keyMaps = listOf(
KeyMap(
@@ -908,7 +981,7 @@ class KeyMapControllerTest {
*/
@Test
fun `imitate button presses when a short press trigger with multiple keys fails`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val trigger = parallelTrigger(
triggerKey(keyCode = 1),
@@ -968,7 +1041,7 @@ class KeyMapControllerTest {
*/
@Test
fun `don't imitate button press when a short press trigger is triggered`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val trigger = parallelTrigger(
triggerKey(keyCode = 1),
@@ -998,7 +1071,7 @@ class KeyMapControllerTest {
*/
@Test
fun `don't repeat when trigger is released for an action that has these options when the trigger is held down`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val action = KeyMapAction(
data = ActionData.InputKeyEvent(keyCode = 1),
@@ -1019,7 +1092,7 @@ class KeyMapControllerTest {
mockTriggerKeyInput(triggerKey(keyCode = 2), delay = 1)
// see if the action repeats
- coroutineScope.testScheduler.apply {
+ testScope.testScheduler.apply {
advanceTimeBy(500)
runCurrent()
}
@@ -1042,7 +1115,7 @@ class KeyMapControllerTest {
*/
@Test
fun `don't initialise repeating if repeat when trigger is released after failed long press`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val trigger1 = parallelTrigger(triggerKey(keyCode = 1))
val action1 = KeyMapAction(
@@ -1092,7 +1165,7 @@ class KeyMapControllerTest {
*/
@Test
fun `don't initialise repeating if repeat when trigger is released after failed failed double press`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val trigger1 = parallelTrigger(triggerKey(keyCode = 1))
val action1 = KeyMapAction(
@@ -1141,7 +1214,7 @@ class KeyMapControllerTest {
*/
@Test
fun `don't initialise repeating if repeat when trigger is released after failed double press and failed long press`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val trigger1 = parallelTrigger(triggerKey(keyCode = 1))
val action1 = KeyMapAction(
@@ -1202,7 +1275,7 @@ class KeyMapControllerTest {
*/
@Test
fun `initialise repeating if repeat until pressed again on failed long press`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val trigger1 = parallelTrigger(triggerKey(keyCode = 1))
val action1 = KeyMapAction(
@@ -1252,7 +1325,7 @@ class KeyMapControllerTest {
*/
@Test
fun `initialise repeating if repeat until pressed again on failed double press`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val trigger1 = parallelTrigger(triggerKey(keyCode = 1))
val action1 = KeyMapAction(
@@ -1307,7 +1380,7 @@ class KeyMapControllerTest {
*/
@Test
fun `initialise repeating if repeat until pressed again on failed double press and failed long press`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val trigger1 = parallelTrigger(triggerKey(keyCode = 1))
val action1 = KeyMapAction(
@@ -1368,7 +1441,7 @@ class KeyMapControllerTest {
*/
@Test
fun `short press key and double press same key sequence trigger, double press key, don't perform action`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val trigger = sequenceTrigger(
triggerKey(KeyEvent.KEYCODE_A),
triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS),
@@ -1392,7 +1465,7 @@ class KeyMapControllerTest {
* issue #563
*/
@Test
- fun sendKeyEventActionWhenImitatingButtonPresses() = coroutineScope.runBlockingTest {
+ fun sendKeyEventActionWhenImitatingButtonPresses() = runTest(testDispatcher) {
val trigger = singleKeyTrigger(
triggerKey(
keyCode = KeyEvent.KEYCODE_META_LEFT,
@@ -1553,7 +1626,7 @@ class KeyMapControllerTest {
@Test
fun `parallel trigger with 2 keys and the 2nd key is another trigger, press 2 key trigger, only the action for 2 key trigger should be performed `() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val twoKeyTrigger = parallelTrigger(
triggerKey(KeyEvent.KEYCODE_SHIFT_LEFT),
@@ -1596,7 +1669,7 @@ class KeyMapControllerTest {
@Test
fun `trigger for a specific device and trigger for any device, input trigger from a different device, only detect trigger for any device`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val triggerKeyboard = singleKeyTrigger(
triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE),
@@ -1623,7 +1696,7 @@ class KeyMapControllerTest {
@Test
fun `trigger for a specific device, input trigger from a different device, do not detect trigger`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val triggerHeadphone = singleKeyTrigger(
triggerKey(KeyEvent.KEYCODE_A, FAKE_HEADPHONE_TRIGGER_KEY_DEVICE),
@@ -1642,7 +1715,7 @@ class KeyMapControllerTest {
@Test
fun `long press trigger and action with Hold Down until pressed again flag, input valid long press, hold down until long pressed again`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val trigger =
singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.LONG_PRESS))
@@ -1684,7 +1757,7 @@ class KeyMapControllerTest {
*/
@Test
fun `trigger with modifier key and modifier keycode action, don't include metastate from the trigger modifier key when an unmapped modifier key is pressed`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_CTRL_LEFT))
keyMapListFlow.value = listOf(
@@ -1747,7 +1820,7 @@ class KeyMapControllerTest {
@Test
fun `2x key sequence trigger and 3x key sequence trigger with the last 2 keys being the same, trigger 3x key trigger, ignore the first 2x key trigger`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
val firstTrigger = sequenceTrigger(
triggerKey(
KeyEvent.KEYCODE_VOLUME_DOWN,
@@ -1784,7 +1857,7 @@ class KeyMapControllerTest {
@Test
fun `2x key long press parallel trigger with HOME or RECENTS keycode, trigger successfully, don't do normal action`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
/*
HOME
*/
@@ -1835,7 +1908,7 @@ class KeyMapControllerTest {
@Test
fun shortPressTriggerDoublePressTrigger_holdDown_onlyDetectDoublePressTrigger() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
val doublePressTrigger = singleKeyTrigger(
@@ -1873,7 +1946,7 @@ class KeyMapControllerTest {
@Test
fun shortPressTriggerLongPressTrigger_holdDown_onlyDetectLongPressTrigger() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
val longPressTrigger = singleKeyTrigger(
@@ -1908,8 +1981,8 @@ class KeyMapControllerTest {
@Test
@Parameters(method = "params_repeatAction")
- fun parallelTrigger_holdDown_repeatAction10Times(description: String, trigger: KeyMapTrigger) =
- coroutineScope.runBlockingTest {
+ fun parallelTrigger_holdDown_repeatAction10Times(description: String, trigger: Trigger) =
+ runTest(testDispatcher) {
// given
val action = KeyMapAction(
data = ActionData.Volume.Up(showVolumeUi = false),
@@ -1957,18 +2030,18 @@ class KeyMapControllerTest {
)
@Test
- @Parameters(method = "params_dualParallelTrigger_input2ndKey_do notConsumeUp")
+ @Parameters(method = "params_dualParallelTrigger_input2ndKey_doNotConsumeUp")
fun dualParallelTrigger_input2ndKey_doNotConsumeUp(
description: String,
- trigger: KeyMapTrigger,
+ trigger: Trigger,
) =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
keyMapListFlow.value =
listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)))
// when
- trigger.keys[1].let {
+ (trigger.keys[1] as KeyCodeTriggerKey).let {
inputKeyEvent(
it.keyCode,
KeyEvent.ACTION_DOWN,
@@ -1976,7 +2049,7 @@ class KeyMapControllerTest {
)
}
- trigger.keys[1].let {
+ (trigger.keys[1] as KeyCodeTriggerKey).let {
val consumed = inputKeyEvent(
it.keyCode,
KeyEvent.ACTION_UP,
@@ -2007,7 +2080,7 @@ class KeyMapControllerTest {
)
@Test
- fun dualShortPressParallelTrigger_validInput_consumeUp() = coroutineScope.runBlockingTest {
+ fun dualShortPressParallelTrigger_validInput_consumeUp() = runTest(testDispatcher) {
// given
val trigger = parallelTrigger(
triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN),
@@ -2018,22 +2091,22 @@ class KeyMapControllerTest {
listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)))
// when
- trigger.keys.forEach {
+ for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) {
inputKeyEvent(
- it.keyCode,
+ key.keyCode,
KeyEvent.ACTION_DOWN,
- triggerKeyDeviceToInputDevice(it.device),
+ triggerKeyDeviceToInputDevice(key.device),
)
}
var consumedUpCount = 0
- trigger.keys.forEach {
+ for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) {
val consumed =
inputKeyEvent(
- it.keyCode,
+ key.keyCode,
KeyEvent.ACTION_UP,
- triggerKeyDeviceToInputDevice(it.device),
+ triggerKeyDeviceToInputDevice(key.device),
)
if (consumed) {
@@ -2046,7 +2119,7 @@ class KeyMapControllerTest {
}
@Test
- fun dualLongPressParallelTrigger_validInput_consumeUp() = coroutineScope.runBlockingTest {
+ fun dualLongPressParallelTrigger_validInput_consumeUp() = runTest(testDispatcher) {
// given
val trigger = parallelTrigger(
triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS),
@@ -2057,11 +2130,11 @@ class KeyMapControllerTest {
listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)))
// when
- trigger.keys.forEach {
+ for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) {
inputKeyEvent(
- it.keyCode,
+ key.keyCode,
KeyEvent.ACTION_DOWN,
- triggerKeyDeviceToInputDevice(it.device),
+ triggerKeyDeviceToInputDevice(key.device),
)
}
@@ -2069,12 +2142,12 @@ class KeyMapControllerTest {
var consumedUpCount = 0
- trigger.keys.forEach {
+ for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) {
val consumed =
inputKeyEvent(
- it.keyCode,
+ key.keyCode,
KeyEvent.ACTION_UP,
- triggerKeyDeviceToInputDevice(it.device),
+ triggerKeyDeviceToInputDevice(key.device),
)
if (consumed) {
@@ -2088,7 +2161,7 @@ class KeyMapControllerTest {
@Test
fun keymappedToLongPressAndDoublePress_invalidLongPress_imitateOnce() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val longPressTrigger = singleKeyTrigger(
triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS),
@@ -2119,7 +2192,7 @@ class KeyMapControllerTest {
@Test
fun keymappedToSingleShortPressAndLongPress_validShortPress_onlyPerformActiondoNotImitateKey() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
@@ -2149,7 +2222,7 @@ class KeyMapControllerTest {
@Test
fun keymappedToShortPressAndDoublePress_validShortPress_onlyPerformActionDoNotImitateKey() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
@@ -2181,7 +2254,7 @@ class KeyMapControllerTest {
@Test
fun singleKeyTriggerAndShortPressParallelTriggerWithSameInitialKey_validSingleKeyTriggerInput_onlyPerformActiondoNotImitateKey() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// given
val singleKeyTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN))
val parallelTrigger = parallelTrigger(
@@ -2209,7 +2282,7 @@ class KeyMapControllerTest {
}
@Test
- fun longPressSequenceTrigger_invalidLongPress_keyImitated() = coroutineScope.runBlockingTest {
+ fun longPressSequenceTrigger_invalidLongPress_keyImitated() = runTest(testDispatcher) {
val trigger = sequenceTrigger(
triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS),
triggerKey(KeyEvent.KEYCODE_VOLUME_UP),
@@ -2232,8 +2305,8 @@ class KeyMapControllerTest {
@Test
@Parameters(method = "params_multipleActionsPerformed")
- fun validInput_multipleActionsPerformed(description: String, trigger: KeyMapTrigger) =
- coroutineScope.runBlockingTest {
+ fun validInput_multipleActionsPerformed(description: String, trigger: Trigger) =
+ runTest(testDispatcher) {
val actionList = listOf(TEST_ACTION, TEST_ACTION_2)
// GIVEN
keyMapListFlow.value = listOf(
@@ -2287,19 +2360,19 @@ class KeyMapControllerTest {
@Parameters(method = "params_allTriggerKeyCombinations")
@TestCaseName("{0}")
fun invalidInput_downNotConsumed(description: String, keyMap: KeyMap) =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
keyMapListFlow.value = listOf(keyMap)
// WHEN
var consumedCount = 0
- keyMap.trigger.keys.forEach {
+ for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) {
val consumed =
inputKeyEvent(
999,
KeyEvent.ACTION_DOWN,
- triggerKeyDeviceToInputDevice(it.device),
+ triggerKeyDeviceToInputDevice(key.device),
)
if (consumed) {
@@ -2315,18 +2388,18 @@ class KeyMapControllerTest {
@Parameters(method = "params_allTriggerKeyCombinations")
@TestCaseName("{0}")
fun validInput_downConsumed(description: String, keyMap: KeyMap) =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
keyMapListFlow.value = listOf(keyMap)
var consumedCount = 0
- keyMap.trigger.keys.forEach {
+ for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) {
val consumed =
inputKeyEvent(
- it.keyCode,
+ key.keyCode,
KeyEvent.ACTION_DOWN,
- triggerKeyDeviceToInputDevice(it.device),
+ triggerKeyDeviceToInputDevice(key.device),
)
if (consumed) {
@@ -2338,20 +2411,20 @@ class KeyMapControllerTest {
}
@Test
- @Parameters(method = "params_allTriggerKeyCombinationsdo notConsume")
+ @Parameters(method = "params_allTriggerKeyCombinationsdoNotConsume")
@TestCaseName("{0}")
fun validInput_doNotConsumeFlag_doNotConsumeDown(description: String, keyMap: KeyMap) =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
keyMapListFlow.value = listOf(keyMap)
var consumedCount = 0
- keyMap.trigger.keys.forEach {
+ for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) {
val consumed =
inputKeyEvent(
- it.keyCode,
+ key.keyCode,
KeyEvent.ACTION_DOWN,
- triggerKeyDeviceToInputDevice(it.device),
+ triggerKeyDeviceToInputDevice(key.device),
)
if (consumed) {
@@ -3119,7 +3192,7 @@ class KeyMapControllerTest {
@Parameters(method = "params_allTriggerKeyCombinations")
@TestCaseName("{0}")
fun validInput_actionPerformed(description: String, keyMap: KeyMap) =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
keyMapListFlow.value = listOf(keyMap)
@@ -3141,6 +3214,10 @@ class KeyMapControllerTest {
}
private suspend fun mockTriggerKeyInput(key: TriggerKey, delay: Long? = null) {
+ if (key !is KeyCodeTriggerKey) {
+ return
+ }
+
val deviceDescriptor = triggerKeyDeviceToInputDevice(key.device)
val pressDuration: Long = delay ?: when (key.clickType) {
ClickType.LONG_PRESS -> LONG_PRESS_DELAY + 100L
@@ -3187,15 +3264,20 @@ class KeyMapControllerTest {
)
private suspend fun mockParallelTrigger(
- trigger: KeyMapTrigger,
+ trigger: Trigger,
delay: Long? = null,
) {
require(trigger.mode is TriggerMode.Parallel)
+ require(trigger.keys.all { it is KeyCodeTriggerKey })
+
+ for (key in trigger.keys) {
+ if (key !is KeyCodeTriggerKey) {
+ continue
+ }
- trigger.keys.forEach {
- val deviceDescriptor = triggerKeyDeviceToInputDevice(it.device)
+ val deviceDescriptor = triggerKeyDeviceToInputDevice(key.device)
- inputKeyEvent(it.keyCode, KeyEvent.ACTION_DOWN, deviceDescriptor)
+ inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, deviceDescriptor)
}
if (delay != null) {
@@ -3208,10 +3290,14 @@ class KeyMapControllerTest {
}
}
- trigger.keys.forEach {
- val deviceDescriptor = triggerKeyDeviceToInputDevice(it.device)
+ for (key in trigger.keys) {
+ if (key !is KeyCodeTriggerKey) {
+ continue
+ }
+
+ val inputDevice = triggerKeyDeviceToInputDevice(key.device)
- inputKeyEvent(it.keyCode, KeyEvent.ACTION_UP, deviceDescriptor)
+ inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice)
}
}
@@ -3244,4 +3330,11 @@ class KeyMapControllerTest {
isGameController = isGameController,
)
}
+
+ private fun mockConstraintSnapshot(isSatisfiedBlock: (constraint: Constraint) -> Boolean) {
+ val snapshot = object : ConstraintSnapshot {
+ override fun isSatisfied(constraint: Constraint): Boolean = isSatisfiedBlock(constraint)
+ }
+ whenever(detectConstraintsUseCase.getSnapshot()).thenReturn(snapshot)
+ }
}
diff --git a/app/src/test/java/io/github/sds100/keymapper/onboarding/FakeOnboardingUseCase.kt b/app/src/test/java/io/github/sds100/keymapper/onboarding/FakeOnboardingUseCase.kt
index accc847383..2cad21b4a9 100644
--- a/app/src/test/java/io/github/sds100/keymapper/onboarding/FakeOnboardingUseCase.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/onboarding/FakeOnboardingUseCase.kt
@@ -26,22 +26,13 @@ class FakeOnboardingUseCase : OnboardingUseCase {
override fun neverShowGuiKeyboardPromptsAgain() {}
- override var approvedFingerprintFeaturePrompt: Boolean = false
override var shownParallelTriggerOrderExplanation: Boolean = false
override var shownSequenceTriggerExplanation: Boolean = false
- override val showFingerprintFeatureNotificationIfAvailable = MutableStateFlow(false)
+ override val showAssistantTriggerFeatureNotification: Flow = MutableStateFlow(false)
- override fun showedFingerprintFeatureNotificationIfAvailable() {}
+ override fun showedAssistantTriggerFeatureNotification() {}
- override val showSetupChosenDevicesAgainNotification = MutableStateFlow(false)
-
- override fun approvedSetupChosenDevicesAgainNotification() {
- approvedSetupChosenDevicesAgainNotification = true
- }
-
- override val showSetupChosenDevicesAgainAppIntro = MutableStateFlow(false)
-
- override fun approvedSetupChosenDevicesAgainAppIntro() {}
+ override var approvedAssistantTriggerFeaturePrompt: Boolean = false
override val showWhatsNew = MutableStateFlow(false)
diff --git a/app/src/test/java/io/github/sds100/keymapper/onboarding/OnboardingUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/onboarding/OnboardingUseCaseTest.kt
index 7c1127eab1..84ad35b46e 100644
--- a/app/src/test/java/io/github/sds100/keymapper/onboarding/OnboardingUseCaseTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/onboarding/OnboardingUseCaseTest.kt
@@ -6,11 +6,10 @@ import io.github.sds100.keymapper.data.repositories.FakePreferenceRepository
import io.github.sds100.keymapper.util.VersionHelper
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineExceptionHandler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.createTestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Before
@@ -27,9 +26,8 @@ import org.mockito.kotlin.mock
@RunWith(MockitoJUnitRunner::class)
class OnboardingUseCaseTest {
- private val testDispatcher = TestCoroutineDispatcher()
- private val coroutineScope =
- createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher)
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private lateinit var useCase: OnboardingUseCaseImpl
private lateinit var fakePreferences: FakePreferenceRepository
@@ -52,7 +50,7 @@ class OnboardingUseCaseTest {
*/
@Test
fun `Only show fingerprint map feature notification for the first update only`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// show it when updating from a version that didn't support it to a version that does
// GIVEN
fakePreferences.set(Keys.approvedFingerprintFeaturePrompt, false)
@@ -100,7 +98,7 @@ class OnboardingUseCaseTest {
@Test
fun `update to 2_3_0, no bluetooth devices were chosen in settings, do not show notification to choose devices again`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
fakePreferences.set(
stringSetPreferencesKey("pref_bluetooth_devices_show_ime_picker"),
@@ -120,7 +118,7 @@ class OnboardingUseCaseTest {
@Test
fun `update to 2_3_0, bluetooth devices were chosen in settings, show notification to choose devices again`() =
- coroutineScope.runBlockingTest {
+ runTest(testDispatcher) {
// GIVEN
fakePreferences.set(
stringSetPreferencesKey("pref_bluetooth_devices_show_ime_picker"),
diff --git a/app/src/test/java/io/github/sds100/keymapper/system/intents/ConfigIntentViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/system/intents/ConfigIntentViewModelTest.kt
index e44012ab60..627ff1d7d9 100644
--- a/app/src/test/java/io/github/sds100/keymapper/system/intents/ConfigIntentViewModelTest.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/system/intents/ConfigIntentViewModelTest.kt
@@ -3,9 +3,14 @@ package io.github.sds100.keymapper.system.intents
import android.content.Intent
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import io.github.sds100.keymapper.util.firstBlocking
+import io.github.sds100.keymapper.util.ui.FakeResourceProvider
+import io.github.sds100.keymapper.util.ui.MultiChoiceItem
+import io.github.sds100.keymapper.util.ui.PopupUi
+import io.github.sds100.keymapper.util.ui.ShowPopupEvent
+import io.github.sds100.keymapper.util.ui.onUserResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.setMain
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasItem
@@ -22,7 +27,7 @@ internal class ConfigIntentViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
- private val testDispatcher = TestCoroutineDispatcher()
+ private val testDispatcher = UnconfinedTestDispatcher()
private lateinit var fakeResourceProvider: FakeResourceProvider
private lateinit var viewModel: ConfigIntentViewModel
@@ -53,7 +58,8 @@ internal class ConfigIntentViewModelTest {
viewModel.showFlagsDialog()
val popupEvent: ShowPopupEvent = viewModel.showPopup.firstBlocking()
val multipleChoiceDialog = popupEvent.ui as PopupUi.MultiChoice<*>
- val expectedCheckedItem = MultiChoiceItem(Intent.FLAG_ACTIVITY_NEW_TASK, "FLAG_ACTIVITY_NEW_TASK", true)
+ val expectedCheckedItem =
+ MultiChoiceItem(Intent.FLAG_ACTIVITY_NEW_TASK, "FLAG_ACTIVITY_NEW_TASK", true)
assertThat(multipleChoiceDialog.items, hasItem(expectedCheckedItem))
}
diff --git a/app/src/testShared/io/github/sds100/keymapper/util/FlowUtils.kt b/app/src/test/java/io/github/sds100/keymapper/util/FlowUtils.kt
similarity index 100%
rename from app/src/testShared/io/github/sds100/keymapper/util/FlowUtils.kt
rename to app/src/test/java/io/github/sds100/keymapper/util/FlowUtils.kt
diff --git a/app/src/testShared/io/github/sds100/keymapper/util/JsonTestUtils.kt b/app/src/test/java/io/github/sds100/keymapper/util/JsonTestUtils.kt
similarity index 96%
rename from app/src/testShared/io/github/sds100/keymapper/util/JsonTestUtils.kt
rename to app/src/test/java/io/github/sds100/keymapper/util/JsonTestUtils.kt
index e98e8edd15..71e2d52856 100644
--- a/app/src/testShared/io/github/sds100/keymapper/util/JsonTestUtils.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/util/JsonTestUtils.kt
@@ -3,6 +3,10 @@ package io.github.sds100.keymapper.util
import com.github.salomonbrys.kotson.contains
import com.github.salomonbrys.kotson.forEach
import com.github.salomonbrys.kotson.get
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.Is.`is`
import org.junit.Assert
diff --git a/app/src/testShared/io/github/sds100/keymapper/util/KeyMapUtils.kt b/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt
similarity index 66%
rename from app/src/testShared/io/github/sds100/keymapper/util/KeyMapUtils.kt
rename to app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt
index aeb5af1aed..fdfaf78f6e 100644
--- a/app/src/testShared/io/github/sds100/keymapper/util/KeyMapUtils.kt
+++ b/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt
@@ -1,7 +1,8 @@
package io.github.sds100.keymapper.util
import io.github.sds100.keymapper.mappings.ClickType
-import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapTrigger
+import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey
+import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice
import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode
@@ -10,17 +11,17 @@ import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode
* Created by sds100 on 19/04/2021.
*/
-fun singleKeyTrigger(key: TriggerKey): KeyMapTrigger = KeyMapTrigger(
+fun singleKeyTrigger(key: TriggerKey): Trigger = Trigger(
keys = listOf(key),
mode = TriggerMode.Undefined,
)
-fun parallelTrigger(vararg keys: TriggerKey): KeyMapTrigger = KeyMapTrigger(
+fun parallelTrigger(vararg keys: TriggerKey): Trigger = Trigger(
keys = keys.toList(),
mode = TriggerMode.Parallel(keys[0].clickType),
)
-fun sequenceTrigger(vararg keys: TriggerKey): KeyMapTrigger = KeyMapTrigger(
+fun sequenceTrigger(vararg keys: TriggerKey): Trigger = Trigger(
keys = keys.toList(),
mode = TriggerMode.Sequence,
)
@@ -30,9 +31,9 @@ fun triggerKey(
device: TriggerKeyDevice = TriggerKeyDevice.Internal,
clickType: ClickType = ClickType.SHORT_PRESS,
consume: Boolean = true,
-): TriggerKey = TriggerKey(
+): KeyCodeTriggerKey = KeyCodeTriggerKey(
keyCode = keyCode,
device = device,
clickType = clickType,
- consumeKeyEvent = consume,
+ consumeEvent = consume,
)
diff --git a/app/src/test/resources/backup-manager-test/restore-keymaps-no-db-version.json b/app/src/test/resources/backup-manager-test/restore-keymaps-no-db-version.json
index 3964dac5e4..b343a2bbdd 100644
--- a/app/src/test/resources/backup-manager-test/restore-keymaps-no-db-version.json
+++ b/app/src/test/resources/backup-manager-test/restore-keymaps-no-db-version.json
@@ -14,6 +14,7 @@
"constraintMode": 1,
"flags": 0,
"id": 0,
+ "uid": "uid1",
"isEnabled": true,
"trigger": {
"extras": [],
@@ -42,6 +43,7 @@
"constraintMode": 1,
"flags": 0,
"id": 0,
+ "uid": "uid2",
"isEnabled": true,
"trigger": {
"extras": [],
diff --git a/app/version.properties b/app/version.properties
index 2d5e7f719b..6723bba07c 100644
--- a/app/version.properties
+++ b/app/version.properties
@@ -1,3 +1,3 @@
-VERSION_NAME=2.6.2
-VERSION_CODE=65
+VERSION_NAME=2.7.0-alpha
+VERSION_CODE=66
VERSION_NUM=0
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index e40c753794..381194ab37 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
buildscript {
- ext.kotlin_version = '1.8.20'
+ ext.kotlin_version = '1.9.22'
repositories {
google()
@@ -14,7 +14,7 @@ buildscript {
def nav_version = '2.6.0'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
- classpath 'com.android.tools.build:gradle:8.4.2'
+ classpath "com.android.tools.build:gradle:8.4.2"
classpath "org.jlleitschuh.gradle:ktlint-gradle:12.1.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
diff --git a/docs/contributing.md b/docs/contributing.md
index ae02854f2e..37e44f496a 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -13,7 +13,7 @@ You can get the apks for the pre-release versions in 2 ways:
There are two types of pre-release versions:
- - **Alpha**. These have ".alpha" in the version name and are the most unstable. Expect the most crashes and broken features in these builds. BEWARE! Your data in Key Mapper isn't considered compatible between alpha builds so it is possible that Key Mapper will crash and refuse to fix itself.
+ - **Alpha**. These have ".alpha" in the version name and are the most unstable. Expect the most crashes and broken features in these builds. BEWARE! Your data in Key Mapper isn't considered compatible between alpha builds so it is possible Key Mapper will crash and refuse to fix itself if you update to a new build that can't understand the data.
- **Beta**. These builds have some of the latest features and contain a few bugs. You can safely update between versions. These have ".beta.X" in the version name. These are pre-release builds for the the open-testing channel on Google Play and F-droid always has beta builds. When all known bugs are fixed a new build is released to the app stores.
### How can I help?
@@ -34,6 +34,17 @@ You can get the apks for the pre-release versions in 2 ways:
!!! info
To build the documentation website you need to install [mkdocs-material](https://squidfunk.github.io/mkdocs-material/getting-started/) with Python. Just run `pip install -r requirements.txt` in the root of the project to install it.
Then run `mkdocs serve` in the project root.
+
+### Build flavors and types
+
+After version 2.7.0 Key Mapper will have 2 build flavours: _free_ and _pro_. The pro flavor includes the closed-source features (e.g assistant trigger) and non-FOSS libraries such as the Google Play Billing library. The free variant stubs out these closed-source features and only uses FOSS libraries.
+
+There are also 4 build types, which have different optimizations and package names.
+
+- **debug** = This is the default debug build type that has no optimizations and builds rapidly. It has a `.debug` package name suffix.
+- **release** = This is the default release build type that includes a lot of optimizations and creates an apk/app bundle suitable for releasing. There is no package name suffix.
+- **debug_release** = This is a debug build type that does not include a package name suffix so that it is possible to test how the production app will look. It is the only way to get the Google Play Billing library functioning because it will break if the package name isn't the same as on the Play store.
+- **ci** = This is used for alpha builds to the community in Discord. It includes some optimizations to ensure it is performant but doesn't obfuscate the code so it is possible to understand logs and bug reports. It has a `.ci` package name suffix.
### Introduction to the structure
@@ -64,9 +75,7 @@ The `system` package bundles all the packages which are related to the Android f
### Branches 🌴
- master: Everything in the latest stable release.
- - develop: The most recent changes. The app is potentially unstable but it can be successfully compiled. A new release is branched off of here.
-
- - release/*: Branched off develop if it is a new large release (e.g 2.4.0), otherwise it is branched off master for bug fix releases (e.g 2.3.1). Beta releases for a particular release are built from here. Once the code is stable, it will be merged into master. No big changes should be made/merged here as the purpose of this branch is to make a release stable. By separating upcoming releases from develop, new features can be worked on in develop without affecting the upcoming release's code base.
+ - develop: The most recent changes. The app is potentially unstable but it can be successfully compiled. A new release is branched off of here.
- feature/*: Any new changes currently being developed. Merges into develop.
- fix/*: A bug fix. This branch should be merged into a release branch and develop.
@@ -142,7 +151,7 @@ Follow Google's Kotlin style guide. [https://developer.android.com/kotlin/style-
## Translating 🌍
You can translate this project on the [CrowdIn page](https://crowdin.com/project/key-mapper). Translations will be
-merged into production once they are >50% translated. If your language isn't available on the CrowdIn page then contact
+merged into production once they are >80% translated. If your language isn't available on the CrowdIn page then contact
the developer so we can add it. Our contact details are in the footer of every page on this site.
We really appreciate translators so thank you! 🙂
diff --git a/docs/images/advanced-triggers-paywall.png b/docs/images/advanced-triggers-paywall.png
new file mode 100644
index 0000000000..d12a5f897e
Binary files /dev/null and b/docs/images/advanced-triggers-paywall.png differ
diff --git a/docs/images/trigger-page.png b/docs/images/trigger-page.png
index a7adaa5457..54d015454a 100644
Binary files a/docs/images/trigger-page.png and b/docs/images/trigger-page.png differ
diff --git a/docs/index.md b/docs/index.md
index d23cfa01e9..ddde1e53e2 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -2,9 +2,9 @@
!!! warning "MAINTENANCE NOTICE!"
- Feature development has stopped.
+ Feature development has slowed down.
-[Key Mapper](http://code.keymapper.club) is a free and open source Android app that can map single or multiple key events to a custom action.
+[Key Mapper](https://github.com/keymapperorg/KeyMapper) is a free and open source Android app that can map single or multiple buttons to a custom action.
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/keymapperorg/KeyMapper.svg)
![GitHub Releases Downloads](https://img.shields.io/github/downloads/keymapperorg/keymapper/total.svg?label=GitHub%20Releases%20Downloads)
@@ -19,12 +19,13 @@ This wiki aims to provide users with a comprehensive guide to using and setting
## Translations
-![cs translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Czech&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27cs%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![es-ES translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Spanish&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27es-ES%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Polish&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27pl%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Russian&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ru%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![sk translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Slovak&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27sk%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
-![zh-CN translation](https://img.shields.io/badge/dynamic/json?color=blue&label=Chinese%20(Simplified)&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27zh-CN%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045.json)
+[![cs proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=cs&style=flat&logo=crowdin&query=%24.progress.1.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![es-ES proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=es-ES&style=flat&logo=crowdin&query=%24.progress.3.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![pl proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=pl&style=flat&logo=crowdin&query=%24.progress.8.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![pt-BR proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=pt-BR&style=flat&logo=crowdin&query=%24.progress.9.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![ru proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=ru&style=flat&logo=crowdin&query=%24.progress.10.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![sk proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=sk&style=flat&logo=crowdin&query=%24.progress.11.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
+[![zh-CN proofreading](https://img.shields.io/badge/dynamic/json?color=green&label=zh-CN&style=flat&logo=crowdin&query=%24.progress.15.data.approvalProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-13864667-360045-update.json)](https://crowdin.com/project/key-mapper)
## Star History
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
index 659bb4595d..94d9808cc7 100644
--- a/docs/overrides/main.html
+++ b/docs/overrides/main.html
@@ -1,5 +1 @@
{% extends "base.html" %}
-
-{% block announce %}
-MAINTENANCE NOTICE! Key Mapper is currently in maintenance mode.
-{% endblock %}
diff --git a/docs/quick-start.md b/docs/quick-start.md
index e0bc1b5ee5..173d073429 100644
--- a/docs/quick-start.md
+++ b/docs/quick-start.md
@@ -182,7 +182,7 @@ You can see explanations of more options [here](../user-guide/keymaps/#special-o
## Managing key maps
-To save your key map and return to the home screen, tap the save :fontawesome-solid-save: icon in the bottom right of the screen.
+To save your key map and return to the home screen, tap the save :material-content-save: icon in the bottom right of the screen.
Now your key map should already be working. To pause/unpause all of your key maps, pull down the notification tray and tap the Key Mapper notification to toggle between Paused and Running.
diff --git a/docs/user-guide/constraints.md b/docs/user-guide/constraints.md
index fecfc5f3b4..f552b58504 100644
--- a/docs/user-guide/constraints.md
+++ b/docs/user-guide/constraints.md
@@ -24,12 +24,12 @@ Your mapping will only work if a specific bluetooth device is connected/disconne
### Orientation (2.2.0+)
This will restrict your gesture map to work only when the device is set to a specific screen orientation.
-### Screen is on/off (ROOT)
+### Screen is on/off
!!! info "Only for key maps"
!!! attention
- You must [grant Key Mapper root permission](settings.md#key-mapper-has-root-permission) and select the [option](../keymaps#special-options) to detect the key map when the screen is off.
+ If you are not using a custom trigger then you must [grant Key Mapper root permission](settings.md#key-mapper-has-root-permission) and select the [option](../keymaps#special-options) to detect the key map when the screen is off.
Only allow the key map to be triggered when the screen is on or off.
diff --git a/docs/user-guide/fingerprint-gestures.md b/docs/user-guide/fingerprint-gestures.md
index 93fc1cc031..82acd15354 100644
--- a/docs/user-guide/fingerprint-gestures.md
+++ b/docs/user-guide/fingerprint-gestures.md
@@ -24,9 +24,9 @@ From the Key Mapper home screen, tab the 'Fingerprint' tab.
Here you can set actions for the 4 directional gestures. Tapping any one of them will bring you to the action assignment screen for that gesture and by tapping 'Add action' at the bottom of the screen you can assign the action. [Click here for an explanation of all the actions you can choose from.](actions.md)
-After choosing an action (or actions) you can press the save :fontawesome-solid-save: icon in the bottom right to save the mapping.
+After choosing an action (or actions) you can press the save :material-content-save: icon in the bottom right to save the mapping.
-Make sure to save :fontawesome-solid-save: your fingerprint gesture map after applying these changes.
+Make sure to save :material-content-save: your fingerprint gesture map after applying these changes.
## Customising actions
diff --git a/docs/user-guide/keymaps.md b/docs/user-guide/keymaps.md
index 8de321abb1..548850ebe7 100644
--- a/docs/user-guide/keymaps.md
+++ b/docs/user-guide/keymaps.md
@@ -1,12 +1,12 @@
Refer to the [Quick Start Guide](../quick-start.md) for help with creating key maps. This page gives more detail about every option.
-Make sure to save :fontawesome-solid-save: your key map after applying these changes.
+Make sure to save :material-content-save: your key map after applying these changes.
## Trigger
A trigger is a combination of keys that must be pressed in a certain way to 'trigger' the key map. A key map can only have one trigger. You can change the order of the keys by holding down on one and then dragging it into a new position.
-This is the page to create a trigger for a key map.
+This is the page to create a trigger for a key map. You will usually want to 'record' a custom trigger that is a combination of physical buttons/keys. There are also ['advanced triggers'](#advanced-triggers) that need to be set up slightly differently.
![](../images/trigger-page.png)
@@ -65,6 +65,49 @@ This will change the click type for a key in a sequence trigger. A parallel trig
option because all the keys have the same click type. You will find the buttons to change a parallel trigger's click
type above the trigger mode buttons as shown in the image at the top of this Trigger section.
+## Advanced triggers
+
+These triggers can be purchased so that the Key Mapper project has a little financial support and the developer is able to invest time maintaining and working on the project. You can see the list of advanced triggers below by tapping 'advanced triggers' on the trigger page.
+
+![](../images/advanced-triggers-paywall.png)
+
+### Assistant trigger
+
+This trigger allows you to remap the various ways that your devices trigger the 'assistant' such as Google Assistant, Bixby, Alexa etc.
+
+There are 3 assistant options you can choose:
+
+- **Device assistant** = This is the assistant usually triggered from a long press of a power button or a dedicated button.
+- **Voice assistant** = This is the assistant launched from the hands-free voice button on keyboards/headsets.
+- **Any assistant** = This will trigger the key map when any of the above are triggered.
+
+!!! note
+ It is not possible to create long-press key maps with this trigger! But you can do double press. You also can not use multiple assistant triggers in a parallel trigger because there is no way to detect them being pressed at exactly the same time.
+
+### Setting up
+
+There are multiple ways of triggering the assistant on different devices.
+
+**Long press power button, Pixel squeeze**
+
+This works on most Android devices. Android devices now have the option for remapping a long press of the power button to the assistant app. Older Pixels, such as the Pixel 2, also had a feature called "Active Edge" that allowed you to _squeeze_ the bottom half of the phone to trigger the assistant. If you select Key Mapper as the 'device assistant' app then your key map will be triggered with both of these methods.
+
+You can set up the long-press of the power button by going to Android Settings :material-arrow-right-thin: System :material-arrow-right-thin: Gestures :material-arrow-right-thin: Press and hold power button. Then choose the digital assistant instead of showing power menu when you long press the power button. Key Mapper will prompt you to select it as the default digital assistant app.
+
+**Bixby button**
+
+This *should* work on Samsung devices that have a dedicated Bixby button but also devices that have the option of remapping the power button to another app when you double press it. You can use the assistant trigger for double pressing the Bixby or power button by picking the 'Assistant trigger' app/activity that shows in your list of apps.
+
+!!! note
+ The developer does not have a Samsung device with a Bixby button so there is no guarantee that it works. If it does, please let the developer know so we can be more confident about support in the future 😄.
+
+**Voice assistant button on keyboards and Bluetooth headphones**
+
+Many external devices such as headsets, keyboards have a button for launching the voice assistant so you can control your phone hands-free. This also works with Key Mapper. If you
+
+!!! warning
+ Some headphones have hardcoded the assistant apps that they will launch and will not work with Key Mapper. The developer has Sony WH1000XM3 headphones that support either Alexa or Google Assistant and refuse to launch Key Mapper when it is selected as the default assistant app.
+
## Customising actions
You can tap the pencil icon :material-pencil-outline: to the right of the action's name to bring up the following menu.
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 14c04c8693..52259dcc03 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -19,40 +19,57 @@ default_platform(:android)
desc "Create testing release"
lane :testing do
- gradle(task: "clean assembleCi")
+ gradle(task: "clean assembleFreeCi")
end
desc "Create and deploy production release"
-lane :prod do |options|
+lane :prod do
version_code = get_properties_value(key: "VERSION_CODE", path: "./app/version.properties")
version_name = get_properties_value(key: "VERSION_NAME", path: "./app/version.properties")
whats_new = File.read("../app/src/main/assets/whats-new.txt")
File.write("metadata/android/en-US/changelogs/" + version_code + ".txt", whats_new)
- gradle(task: "assembleDebug")
- gradle(task: "assembleRelease")
- gradle(task: "bundleRelease")
+ gradle(task: "testDebugUnitTest")
- apk_path_debug="app/build/outputs/apk/debug/keymapper-" + version_name + "-debug.apk"
- apk_path_release="app/build/outputs/apk/release/keymapper-" + version_name + ".apk"
+ github_token = prompt(
+ text: "Github token: ",
+ secure_text: true
+ )
- supply(
- aab: "./app/build/outputs/bundle/release/app-release.aab",
- track: "beta",
- release_status: "draft",
- skip_upload_apk: true
- )
+ ENV["KEYSTORE_PASSWORD"] = prompt(
+ text: "Key store password: ",
+ secure_text: true
+ )
+
+ ENV["KEY_PASSWORD"] = prompt(
+ text: "Key password: ",
+ secure_text: true
+ )
+
+# Do not release a debug build for pro version.
+# gradle(task: "assembleDebug")
+ gradle(task: "assembleProRelease")
+ gradle(task: "bundleProRelease")
+
+ apk_path_release="app/build/outputs/apk/pro/release/keymapper-" + version_name + ".apk"
github_release = set_github_release(
repository_name: "keymapperorg/KeyMapper",
- api_bearer: options[:github_token],
+ api_bearer: github_token,
name: version_name,
tag_name: "v" + version_name,
description: whats_new,
commitish: "master",
- upload_assets: [apk_path_debug, apk_path_release],
+ upload_assets: [apk_path_release],
is_draft: false,
is_prerelease: false
)
-end
\ No newline at end of file
+
+ supply(
+ aab: "app/build/outputs/bundle/proRelease/app-pro-release.aab",
+ track: "beta",
+ release_status: "draft",
+ skip_upload_apk: true
+ )
+end
diff --git a/fastlane/README.md b/fastlane/README.md
index e799eb9f9e..19f837294e 100644
--- a/fastlane/README.md
+++ b/fastlane/README.md
@@ -19,7 +19,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
[bundle exec] fastlane testing
```
-Create and deploy testing release
+Create testing release
### prod
diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt
index aba234fb7b..fcbde9987d 100644
--- a/fastlane/metadata/android/ar/full_description.txt
+++ b/fastlane/metadata/android/ar/full_description.txt
@@ -1,9 +1,9 @@
ما الذي يمكن إعادة تعيينه؟
- * إيماءات بصمات الأصابع على الأجهزة المدعومة.
* أزرار الصوت.
- * أزرار التنقل.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* بلوتوث / لوحات المفاتيح السلكية.
+ * إيماءات بصمات الأصابع على الأجهزة المدعومة.
* يجب أن تعمل الأزرار الموجودة على الأجهزة المتصلة الأخرى أيضا.
يمكن إعادة تعيين أزرار الأجهزة فقط.
@@ -12,12 +12,10 @@
يمكنك الجمع بين مفاتيح متعددة من جهاز معين أو أي جهاز لتشكيل "المشغل". يمكن أن يكون لكل مشغل إجراءات متعددة. يمكن ضبط المفاتيح للضغط عليها في نفس الوقت أو واحدة تلو الأخرى في تسلسل. يمكن إعادة تعيين المفاتيح عند الضغط عليها لفترة قصيرة أو الضغط عليها لفترة طويلة أو الضغط عليها مرتين. يمكن أن تحتوي خريطة المفاتيح على مجموعة من "القيود" بحيث يكون لها التأثير فقط في مواقف معينة.
ما الذي لا يمكن إعادة تعيينه؟
- * زر الطاقة
- * زر بيكسبي
* أزرار الماوس
* * دباد ، عصي الإبهام أو المشغلات على أجهزة التحكم في اللعبة
-لا تعمل خرائط المفاتيح إذا كانت الشاشة متوقفة عن التشغيل. هذا مقيد في Android. لا يوجد شيء يمكن للمطور القيام به حيال ذلك.
+Your key maps do not work if the screen is OFF. هذا مقيد في Android. لا يوجد شيء يمكن للمطور القيام به حيال ذلك.
ما الذي يمكنني لإعادة تعيين مفاتيحي للقيام به؟
ستعمل بعض الإجراءات فقط على الأجهزة التي تم عمل روت لها وإصدارات Android محددة.
@@ -27,7 +25,7 @@
الصلاحيات
لا يلزمك منح جميع الأذونات حتى يعمل التطبيق. سيخبرك التطبيق ما إذا كان يجب منح إذن حتى تعمل الميزة.
- * خدمة إمكانية الوصول: الشرط الأساسي لإعادة التعيين لأجل العمل. هناك حاجة إليه حتى يتمكن التطبيق من الاستماع إلى الأحداث الرئيسية وحظرها.
+ * خدمة إمكانية الوصول: الشرط الأساسي لإعادة التعيين لأجل العمل. It is needed so the app can listen to and block key events.
* مسؤول الجهاز: لإيقاف تشغيل الشاشة عند استخدام الإجراء لإيقاف تشغيل الشاشة.
* تعديل إعدادات النظام: لتغيير إعدادات السطوع والدوران.
* الكاميرا: للتحكم في الكشاف.
diff --git a/fastlane/metadata/android/cs_CZ/full_description.txt b/fastlane/metadata/android/cs_CZ/full_description.txt
index 6591796b18..8f81c40a35 100644
--- a/fastlane/metadata/android/cs_CZ/full_description.txt
+++ b/fastlane/metadata/android/cs_CZ/full_description.txt
@@ -1,9 +1,9 @@
Co lze přemapovat?
- * Gesta otisků prstů na podporovaných zařízeních.
* Tlačítka hlasitosti.
- * Navigační tlačítka.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/kabelová klávesnice.
+ * Gesta otisků prstů na podporovaných zařízeních.
* Tlačítka na ostatních připojených zařízeních by měla také fungovat.
POUZE HARDWARE tlačítka mohou být znovu mapována.
@@ -12,12 +12,10 @@ POUZE HARDWARE tlačítka mohou být znovu mapována.
Můžete kombinovat více klíčů z konkrétního zařízení nebo jakéhokoli zařízení a vytvořit "trigger". Každý spouštěč může mít více akcí. Klávesy lze nastavit tak, aby byly stisknuty současně nebo jednou za sebou. Tlačítka mohou být zmapována po krátkém stisknutí, dlouhém stisknutí nebo dvojitém stisknutí. Klíčová mapa může mít sadu "vazeb", takže má vliv pouze v určitých situacích.
Co nelze přemapovat?
- * Tlačítko napájení
- * Tlačítko Bixby
* Tlačítka myši
* Dpad, paličky nebo spouštěče na herních ovladačích
-Vaše klíčové mapy nefungují, pokud je obrazovka vypnutá. Toto je omezení v Android. Není nic, co by vývojář nemohl udělat.
+Your key maps do not work if the screen is OFF. Toto je omezení v Android. Není nic, co by vývojář nemohl udělat.
Co mohu přemapovat své klíče?
Některé akce budou fungovat pouze na rootovaných zařízeních a konkrétních verzích Androidu.
@@ -27,7 +25,7 @@ Existuje příliš mnoho funkcí, které zde můžete vypsaovat, takže se podí
Skupiny uživatelů a dokumentů
Není nutné udělovat všechna oprávnění pro fungování aplikace. Aplikace vám řekne, zda má být uděleno oprávnění pro funkčnost funkce.
- * Služba přístupu: Základní požadavek pro přemapování do práce. Je to potřeba, aby aplikace mohla poslouchat a blokovat klíčové události.
+ * Služba přístupu: Základní požadavek pro přemapování do práce. It is needed so the app can listen to and block key events.
* Admin zařízení: Při použití akce vypněte obrazovku.
* Upravit nastavení systému: Chcete-li změnit nastavení jasu a rotace.
* Fotoaparát: Pro ovládání svítilny.
diff --git a/fastlane/metadata/android/de_DE/full_description.txt b/fastlane/metadata/android/de_DE/full_description.txt
index 515ccecd74..13f13d37dd 100644
--- a/fastlane/metadata/android/de_DE/full_description.txt
+++ b/fastlane/metadata/android/de_DE/full_description.txt
@@ -1,9 +1,9 @@
What can be remapped?
- * Fingerprint gestures on supported devices.
* Volume buttons.
- * Navigation buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
* Buttons on other connected devices should also work.
ONLY HARDWARE buttons can be remapped.
@@ -12,12 +12,10 @@ There is NO GUARANTEE any of these buttons will work and this app is NOT designe
You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
What can’t be remapped?
- * Power button
- * Bixby button
* Mouse buttons
* Dpad, thumb sticks or triggers on game controllers
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
What can I remap my keys to do?
Some actions will only work on rooted devices and specific Android versions.
@@ -27,7 +25,7 @@ There are too many features to list here so check out the full list here: https:
Permissions
You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
* Device Admin: To turn the screen off when using the action to turn off the screen.
* Modify System Settings: To change the brightness and rotation settings.
* Camera: To control the flashlight.
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 515ccecd74..13f13d37dd 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -1,9 +1,9 @@
What can be remapped?
- * Fingerprint gestures on supported devices.
* Volume buttons.
- * Navigation buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
* Buttons on other connected devices should also work.
ONLY HARDWARE buttons can be remapped.
@@ -12,12 +12,10 @@ There is NO GUARANTEE any of these buttons will work and this app is NOT designe
You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
What can’t be remapped?
- * Power button
- * Bixby button
* Mouse buttons
* Dpad, thumb sticks or triggers on game controllers
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
What can I remap my keys to do?
Some actions will only work on rooted devices and specific Android versions.
@@ -27,7 +25,7 @@ There are too many features to list here so check out the full list here: https:
Permissions
You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
* Device Admin: To turn the screen off when using the action to turn off the screen.
* Modify System Settings: To change the brightness and rotation settings.
* Camera: To control the flashlight.
diff --git a/fastlane/metadata/android/es_ES/full_description.txt b/fastlane/metadata/android/es_ES/full_description.txt
index 40b79cf500..5181a46cc0 100644
--- a/fastlane/metadata/android/es_ES/full_description.txt
+++ b/fastlane/metadata/android/es_ES/full_description.txt
@@ -1,9 +1,9 @@
¿Qué se puede reasignar?
- * Gestos con la huella dactilar en los dispositivos compatibles.
* Botones de volumen.
- * Botones de navegación.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Teclados Bluetooth/con cable.
+ * Gestos con la huella dactilar en los dispositivos compatibles.
* Los botones de otros dispositivos conectados también deberían funcionar.
Sólo se pueden reasignar los botones de HARDWARE.
@@ -12,12 +12,10 @@ No hay garantía de que ninguno de estos botones funcione y esta aplicación NO
Puedes combinar varias teclas de un dispositivo específico o de cualquier dispositivo para formar un "activador". Cada activador puede tener múltiples acciones. Las teclas pueden configurarse para ser pulsadas al mismo tiempo o una tras otra en una secuencia. Las teclas se pueden reasignar cuando se presionan brevemente, se presionan prolongadamente o se presionan dos veces. Un mapa de teclas puede tener un conjunto de "restricciones" para que sólo tenga efecto en determinadas situaciones.
¿Qué no es posible remapear?
- * Botón de encendido
- * Botón Bixby
* Botones del ratón
* Dpad, thumb sticks o gatillos en los mandos de los juegos
-Sus mapas de teclas no funcionan si la pantalla está apagada. Esta es una limitación de Android. No hay nada que pueda hacer el desarrollador.
+Your key maps do not work if the screen is OFF. Esta es una limitación de Android. No hay nada que pueda hacer el desarrollador.
¿Qué puedo hacer con mis teclas?
Algunas acciones sólo funcionarán en dispositivos rooteados y en versiones específicas de Android.
@@ -27,7 +25,7 @@ Hay demasiadas características para enumerarlas, así que echa un vistazo a la
Permisos
No tienes que conceder todos los permisos para que la aplicación funcione. La aplicación te dirá si es necesario conceder un permiso para que una característica funcione.
- * Servicio de accesibilidad: Requisito básico para que funcione la reasignación. Es necesario para que la aplicación pueda escuchar y bloquear los keyyevents.
+ * Servicio de accesibilidad: Requisito básico para que funcione la reasignación. It is needed so the app can listen to and block key events.
* Administración del dispositivo: Para apagar la pantalla cuando se utiliza la acción de apagar la pantalla.
* Modificar los ajustes del sistema: Para cambiar los ajustes de brillo y rotación.
* Cámara: Para controlar la linterna.
diff --git a/fastlane/metadata/android/fr_FR/full_description.txt b/fastlane/metadata/android/fr_FR/full_description.txt
index 515ccecd74..13f13d37dd 100644
--- a/fastlane/metadata/android/fr_FR/full_description.txt
+++ b/fastlane/metadata/android/fr_FR/full_description.txt
@@ -1,9 +1,9 @@
What can be remapped?
- * Fingerprint gestures on supported devices.
* Volume buttons.
- * Navigation buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
* Buttons on other connected devices should also work.
ONLY HARDWARE buttons can be remapped.
@@ -12,12 +12,10 @@ There is NO GUARANTEE any of these buttons will work and this app is NOT designe
You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
What can’t be remapped?
- * Power button
- * Bixby button
* Mouse buttons
* Dpad, thumb sticks or triggers on game controllers
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
What can I remap my keys to do?
Some actions will only work on rooted devices and specific Android versions.
@@ -27,7 +25,7 @@ There are too many features to list here so check out the full list here: https:
Permissions
You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
* Device Admin: To turn the screen off when using the action to turn off the screen.
* Modify System Settings: To change the brightness and rotation settings.
* Camera: To control the flashlight.
diff --git a/fastlane/metadata/android/hu_HU/full_description.txt b/fastlane/metadata/android/hu_HU/full_description.txt
index caee3e7d7d..8fd2ebd525 100644
--- a/fastlane/metadata/android/hu_HU/full_description.txt
+++ b/fastlane/metadata/android/hu_HU/full_description.txt
@@ -1,9 +1,9 @@
Mit lehet átállítani?
- * Ujjlenyomat-gesztusok a támogatott eszközökön.
* Hangerőszabályzó gombok.
- * Navigációs gombok.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/vezetékes billentyűzeteket.
+ * Ujjlenyomat-gesztusok a támogatott eszközökön.
* Más csatlakoztatott eszközök gombjainak is működniük kell.
Kizárólag a HARDVERes gombokat lehet átképezni.
@@ -12,12 +12,10 @@ NINCS GARANCIA, hogy bármelyik gomb működni fog, és ezt az alkalmazást NEM
Egy adott eszköz vagy bármely eszköz több billentyűjét kombinálhatod aktiválásként. Minden egyes aktiváláshoz több művelet is tartozhat. A billentyűk egyszerre vagy egymás után, sorrendben nyomhatók le. A billentyűk rövid, hosszú vagy dupla megnyomás esetén átállíthatóak. A billentyű-térképnek lehet egy sor "korlátozása", így csak bizonyos helyzetekben van hatása.
Mit nem lehet átállítani?
- * Bekapcsoló gomb
- * Bixby gomb
* Egér gombok
* Dpad, hüvelykujjas botok vagy ravaszok a játékvezérlőkön
-A billentyűs térképek nem működnek, ha a képernyő ki van kapcsolva. Ez az Android egyik korlátozása. A fejlesztő semmit sem tehet ez ellen.
+Your key maps do not work if the screen is OFF. Ez az Android egyik korlátozása. A fejlesztő semmit sem tehet ez ellen.
Mit tudok a billentyűimnek beállítani?
Egyes műveletek csak rootolt eszközökön és bizonyos Android-verziókon működnek.
@@ -27,7 +25,7 @@ Túl sok funkció van ahhoz, hogy itt felsoroljuk, ezért a teljes listát itt t
Engedélyek
Nem kell minden engedélyt megadnod ahhoz, hogy az alkalmazás működjön. Az alkalmazás megmondja, ha egy funkció működéséhez engedélyt kell adni.
- * Akadálymentesítési szolgáltatás: Alapvető követelmény az átállításhoz. Erre azért van szükség, hogy az alkalmazás figyelni és blokkolni tudja a billentyűlenyomásokat.
+ * Akadálymentesítési szolgáltatás: Alapvető követelmény az átállításhoz. It is needed so the app can listen to and block key events.
* Adminisztrátor: A képernyő kikapcsolása a képernyő kikapcsolására szolgáló művelet használatakor.
* Rendszerbeállítások módosítása: A fényerő és a forgatási beállítások megváltoztatásához.
* Kamera: A zseblámpa vezérléséhez.
diff --git a/fastlane/metadata/android/ka_GE/full_description.txt b/fastlane/metadata/android/ka_GE/full_description.txt
index 515ccecd74..13f13d37dd 100644
--- a/fastlane/metadata/android/ka_GE/full_description.txt
+++ b/fastlane/metadata/android/ka_GE/full_description.txt
@@ -1,9 +1,9 @@
What can be remapped?
- * Fingerprint gestures on supported devices.
* Volume buttons.
- * Navigation buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
* Buttons on other connected devices should also work.
ONLY HARDWARE buttons can be remapped.
@@ -12,12 +12,10 @@ There is NO GUARANTEE any of these buttons will work and this app is NOT designe
You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
What can’t be remapped?
- * Power button
- * Bixby button
* Mouse buttons
* Dpad, thumb sticks or triggers on game controllers
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
What can I remap my keys to do?
Some actions will only work on rooted devices and specific Android versions.
@@ -27,7 +25,7 @@ There are too many features to list here so check out the full list here: https:
Permissions
You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
* Device Admin: To turn the screen off when using the action to turn off the screen.
* Modify System Settings: To change the brightness and rotation settings.
* Camera: To control the flashlight.
diff --git a/fastlane/metadata/android/ko_KR/full_description.txt b/fastlane/metadata/android/ko_KR/full_description.txt
index 515ccecd74..13f13d37dd 100644
--- a/fastlane/metadata/android/ko_KR/full_description.txt
+++ b/fastlane/metadata/android/ko_KR/full_description.txt
@@ -1,9 +1,9 @@
What can be remapped?
- * Fingerprint gestures on supported devices.
* Volume buttons.
- * Navigation buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
* Buttons on other connected devices should also work.
ONLY HARDWARE buttons can be remapped.
@@ -12,12 +12,10 @@ There is NO GUARANTEE any of these buttons will work and this app is NOT designe
You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
What can’t be remapped?
- * Power button
- * Bixby button
* Mouse buttons
* Dpad, thumb sticks or triggers on game controllers
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
What can I remap my keys to do?
Some actions will only work on rooted devices and specific Android versions.
@@ -27,7 +25,7 @@ There are too many features to list here so check out the full list here: https:
Permissions
You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
* Device Admin: To turn the screen off when using the action to turn off the screen.
* Modify System Settings: To change the brightness and rotation settings.
* Camera: To control the flashlight.
diff --git a/fastlane/metadata/android/pl_PL/full_description.txt b/fastlane/metadata/android/pl_PL/full_description.txt
index 506a022419..2b2b73c63a 100644
--- a/fastlane/metadata/android/pl_PL/full_description.txt
+++ b/fastlane/metadata/android/pl_PL/full_description.txt
@@ -1,9 +1,9 @@
Funkcje jakich przycisków można zmienić?
- * Gesty czytnika linii papilarnych na obsługiwanych urządzeniach.
* Przyciski głośności.
- * Przyciski nawigacji.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Klawiatury przewodowe i Bluetooth.
+ * Gesty czytnika linii papilarnych na obsługiwanych urządzeniach.
* Przyciski na innych podłączonych urządzeniach również powinny działać.
TYLKO FIZYCZNE przyciski mogą mieć zmienione funkcje.
@@ -12,12 +12,10 @@ NIE MA GWARANCJI, że Twoje przyciski mogą być używane, a także ta aplikacja
Możesz połączyć wiele przycisków z określonego lub dowolnego urządzenia, aby utworzyć "wyzwalacz". Każdy wyzwalacz może mieć wiele czynności. Przyciski można ustawić tak, aby były naciskane jednocześnie lub jeden po drugim w kolejności. Przyciski mogą być wykorzystane, gdy są wciśnięte krótko, długo lub podwójnie. Zmiana funkcji może mieć zestaw "ograniczeń", aby działała tylko w określonych sytuacjach.
Funkcje jakich przycisków nie można zmieniać?
- * Przycisk zasilania
- * Przycisk Bixby
* Przyciski myszy
* Krzyżak, gałki albo spusty na kontrolerach gier
-Twoje zmiany funkcji nie będą działać, gdy ekran jest WYŁĄCZONY. To jest ograniczenie w systemie Android. Programista nic nie może na to poradzić.
+Your key maps do not work if the screen is OFF. To jest ograniczenie w systemie Android. Programista nic nie może na to poradzić.
Jakie funkcje można przypisać do przycisków?
Niektóre czynności będą działać tylko na zrootowanych urządzeniach i określonych wersjach Androida.
@@ -27,7 +25,7 @@ Mamy zbyt wiele funkcji, aby wszystko się tu zmieściło, więc zapoznaj się z
Uprawnienia
Nie musisz przyznawać wszystkich uprawnień, aby aplikacja działała. Aplikacja poinformuje Cię, czy do działania funkcji wymagane jest określone uprawnienie.
- * Usługa ułatwień dostępu: Podstawowe wymaganie dotyczące zmiany funkcji przycisków. Jest to potrzebne, aby aplikacja mogła nasłuchiwać i blokować zdarzenia przycisków.
+ * Usługa ułatwień dostępu: Podstawowe wymaganie dotyczące zmiany funkcji przycisków. It is needed so the app can listen to and block key events.
* Zarządzanie urządzeniem: aby wyłączyć ekran podczas korzystania z czynności wyłączenia ekranu.
* Modyfikowanie ustawień systemu: Aby zmienić ustawienia jasności i obracania.
* Aparat: Aby sterować latarką.
diff --git a/fastlane/metadata/android/pt_BR/full_description.txt b/fastlane/metadata/android/pt_BR/full_description.txt
index 515ccecd74..0a8a31bb4a 100644
--- a/fastlane/metadata/android/pt_BR/full_description.txt
+++ b/fastlane/metadata/android/pt_BR/full_description.txt
@@ -1,38 +1,36 @@
-What can be remapped?
+O que pode ser remapeado?
- * Fingerprint gestures on supported devices.
- * Volume buttons.
- * Navigation buttons.
- * Bluetooth/wired keyboards.
- * Buttons on other connected devices should also work.
+ * Botões de volume.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
+ * Teclados Bluetooth/com fio.
+ * Gestos de impressão digital em dispositivos suportados.
+ * Botões em outros dispositivos conectados também devem funcionar.
-ONLY HARDWARE buttons can be remapped.
-There is NO GUARANTEE any of these buttons will work and this app is NOT designed to control games. Your device's OEM/vendor can prevent them from being remapped.
+SOMENTE botões de HARDWARE podem ser remapeados.
+NÃO HÁ GARANTIA de que qualquer um desses botões funcionará e este aplicativo NÃO foi projetado para controlar jogos. O OEM/fornecedor do seu dispositivo pode impedir que ele seja remapeado.
-You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
+Você pode combinar várias teclas de um dispositivo específico ou de qualquer dispositivo para formar um "gatilho". Cada gatilho pode ter múltiplas ações. As teclas podem ser configuradas para serem pressionadas ao mesmo tempo ou uma após a outra em sequência. As teclas podem ser remapeadas quando pressionadas brevemente, longamente ou duas vezes. Um mapa de teclas pode ter um conjunto de "restrições" para que ele só tenha efeito em determinadas situações.
-What can’t be remapped?
- * Power button
- * Bixby button
- * Mouse buttons
- * Dpad, thumb sticks or triggers on game controllers
+O que não pode ser remapeado?
+ * Botões do mouse
+ * Dpad, joysticks ou gatilhos em controles de jogos
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. Esta é uma limitação do Android. Não há nada que o desenvolvedor possa fazer.
-What can I remap my keys to do?
-Some actions will only work on rooted devices and specific Android versions.
+O que posso fazer para remapear minhas chaves?
+Algumas ações só funcionarão em dispositivos com root e em versões específicas do Android.
-There are too many features to list here so check out the full list here: https://docs.keymapper.club/user-guide/actions
+Há muitos recursos para listar aqui, então confira a lista completa aqui: https://docs.keymapper.club/user-guide/actions
-Permissions
-You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
+Permissões
+Você não precisa conceder todas as permissões para que o aplicativo funcione. O aplicativo informará se é necessário conceder uma permissão para que um recurso funcione.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
- * Device Admin: To turn the screen off when using the action to turn off the screen.
- * Modify System Settings: To change the brightness and rotation settings.
- * Camera: To control the flashlight.
+ * Serviço de Acessibilidade: Requisito básico para o remapeamento para o trabalho. It is needed so the app can listen to and block key events.
+ * Administrador do dispositivo: para desligar a tela ao usar a ação para desligar a tela.
+ * Modificar configurações do sistema: para alterar as configurações de brilho e rotação.
+ * Câmera: Para controlar a lanterna.
- On some devices, enabling the accessibility service will disable "enhanced data encryption".
+ Em alguns dispositivos, habilitar o serviço de acessibilidade desabilitará a "criptografia de dados aprimorada".
Discord: www.keymapper.club
-Website: docs.keymapper.club
\ No newline at end of file
+Site: docs.keymapper.club
\ No newline at end of file
diff --git a/fastlane/metadata/android/pt_BR/short_description.txt b/fastlane/metadata/android/pt_BR/short_description.txt
index 4de8bd060b..e056032ee7 100644
--- a/fastlane/metadata/android/pt_BR/short_description.txt
+++ b/fastlane/metadata/android/pt_BR/short_description.txt
@@ -1 +1 @@
-Unleash your keys! Open source!
\ No newline at end of file
+Liberte suas chaves! Código aberto!
\ No newline at end of file
diff --git a/fastlane/metadata/android/ru_RU/full_description.txt b/fastlane/metadata/android/ru_RU/full_description.txt
index ed0b6074cc..5fd8fd474b 100644
--- a/fastlane/metadata/android/ru_RU/full_description.txt
+++ b/fastlane/metadata/android/ru_RU/full_description.txt
@@ -1,9 +1,9 @@
Что можно переназначить?
- * Жесты отпечатка пальца на поддерживаемых устройствах.
* Кнопки громкости.
- * Кнопки навигации.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Проводные и Bluetooth клавиатуры.
+ * Жесты отпечатка пальца на поддерживаемых устройствах.
* Кнопки на других подключенных устройствах также должны работать.
Переназначены могут быть ТОЛЬКО ФИЗИЧЕСКИЕ кнопки.
@@ -12,12 +12,10 @@
Вы можете совместить нажатие нескольких кнопок на определенном устройстве или нескольких устройствах для создания "события". У каждого события может быть несколько действий. Можно назначить событие на одновременное или последовательное нажатие клавиш. Клавиши можно переназначить при их коротком, длительном или двойном нажатии. Переназначение может иметь набор "ограничений" для того чтобы срабатывать только в определенных ситуациях.
Что НЕ может быть переназначено:
- * Кнопка питания
- * Кнопка Bixby
* Кнопки мыши
* Dpad, джойстики или триггеры на игровых контроллерах
-Переназначения клавиш не работают когда экран вашего устройства выключен. Это ограничение самого Android. К сожалению с этим ничего нельзя сделать.
+Your key maps do not work if the screen is OFF. Это ограничение самого Android. К сожалению с этим ничего нельзя сделать.
Каким образом можно переназначить клавиши?
Некоторые действия будут работать только на устройствах с Root правами и/или на конкретных версиях Android.
@@ -27,7 +25,7 @@
Разрешения
Вам не нужно предоставлять все разрешения для работы приложения. Приложение сообщит, нужно ли вам разрешение для работы функции.
- * Служба спец.возможностей: Базовое требование для работы переназначения клавиш. Это необходимо, чтобы приложение могло слушать и блокировать события нажатий.
+ * Служба спец.возможностей: Базовое требование для работы переназначения клавиш. It is needed so the app can listen to and block key events.
* Администратор устройства: Чтобы выключить экран при использовании действия для выключения экрана.
* Изменение системных настроек: для изменения яркости и параметров вращения.
* Камера: Для управления фонариком.
diff --git a/fastlane/metadata/android/sk/full_description.txt b/fastlane/metadata/android/sk/full_description.txt
index 515ccecd74..13f13d37dd 100644
--- a/fastlane/metadata/android/sk/full_description.txt
+++ b/fastlane/metadata/android/sk/full_description.txt
@@ -1,9 +1,9 @@
What can be remapped?
- * Fingerprint gestures on supported devices.
* Volume buttons.
- * Navigation buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
* Buttons on other connected devices should also work.
ONLY HARDWARE buttons can be remapped.
@@ -12,12 +12,10 @@ There is NO GUARANTEE any of these buttons will work and this app is NOT designe
You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
What can’t be remapped?
- * Power button
- * Bixby button
* Mouse buttons
* Dpad, thumb sticks or triggers on game controllers
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
What can I remap my keys to do?
Some actions will only work on rooted devices and specific Android versions.
@@ -27,7 +25,7 @@ There are too many features to list here so check out the full list here: https:
Permissions
You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
* Device Admin: To turn the screen off when using the action to turn off the screen.
* Modify System Settings: To change the brightness and rotation settings.
* Camera: To control the flashlight.
diff --git a/fastlane/metadata/android/tr_TR/full_description.txt b/fastlane/metadata/android/tr_TR/full_description.txt
index 515ccecd74..13f13d37dd 100644
--- a/fastlane/metadata/android/tr_TR/full_description.txt
+++ b/fastlane/metadata/android/tr_TR/full_description.txt
@@ -1,9 +1,9 @@
What can be remapped?
- * Fingerprint gestures on supported devices.
* Volume buttons.
- * Navigation buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
* Buttons on other connected devices should also work.
ONLY HARDWARE buttons can be remapped.
@@ -12,12 +12,10 @@ There is NO GUARANTEE any of these buttons will work and this app is NOT designe
You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
What can’t be remapped?
- * Power button
- * Bixby button
* Mouse buttons
* Dpad, thumb sticks or triggers on game controllers
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
What can I remap my keys to do?
Some actions will only work on rooted devices and specific Android versions.
@@ -27,7 +25,7 @@ There are too many features to list here so check out the full list here: https:
Permissions
You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
* Device Admin: To turn the screen off when using the action to turn off the screen.
* Modify System Settings: To change the brightness and rotation settings.
* Camera: To control the flashlight.
diff --git a/fastlane/metadata/android/uk_UA/full_description.txt b/fastlane/metadata/android/uk_UA/full_description.txt
new file mode 100644
index 0000000000..13f13d37dd
--- /dev/null
+++ b/fastlane/metadata/android/uk_UA/full_description.txt
@@ -0,0 +1,36 @@
+What can be remapped?
+
+ * Volume buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
+ * Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
+ * Buttons on other connected devices should also work.
+
+ONLY HARDWARE buttons can be remapped.
+There is NO GUARANTEE any of these buttons will work and this app is NOT designed to control games. Your device's OEM/vendor can prevent them from being remapped.
+
+You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
+
+What can’t be remapped?
+ * Mouse buttons
+ * Dpad, thumb sticks or triggers on game controllers
+
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+
+What can I remap my keys to do?
+Some actions will only work on rooted devices and specific Android versions.
+
+There are too many features to list here so check out the full list here: https://docs.keymapper.club/user-guide/actions
+
+Permissions
+You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
+
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
+ * Device Admin: To turn the screen off when using the action to turn off the screen.
+ * Modify System Settings: To change the brightness and rotation settings.
+ * Camera: To control the flashlight.
+
+ On some devices, enabling the accessibility service will disable "enhanced data encryption".
+
+Discord: www.keymapper.club
+Website: docs.keymapper.club
\ No newline at end of file
diff --git a/fastlane/metadata/android/uk_UA/short_description.txt b/fastlane/metadata/android/uk_UA/short_description.txt
new file mode 100644
index 0000000000..4de8bd060b
--- /dev/null
+++ b/fastlane/metadata/android/uk_UA/short_description.txt
@@ -0,0 +1 @@
+Unleash your keys! Open source!
\ No newline at end of file
diff --git a/fastlane/metadata/android/uk_UA/title.txt b/fastlane/metadata/android/uk_UA/title.txt
new file mode 100644
index 0000000000..19f819ebd7
--- /dev/null
+++ b/fastlane/metadata/android/uk_UA/title.txt
@@ -0,0 +1 @@
+Key Mapper
\ No newline at end of file
diff --git a/fastlane/metadata/android/vi_VN/full_description.txt b/fastlane/metadata/android/vi_VN/full_description.txt
new file mode 100644
index 0000000000..a3b5ab41a9
--- /dev/null
+++ b/fastlane/metadata/android/vi_VN/full_description.txt
@@ -0,0 +1,36 @@
+Những gì có thể được ánh xạ lại?
+
+ * Nút âm lượng.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
+ * Bàn phím Bluetooth/có dây.
+ * Cử chỉ vân tay trên các thiết bị được hỗ trợ.
+ * Các nút trên các thiết bị được kết nối khác cũng sẽ hoạt động.
+
+CHỈ có thể ánh xạ lại các nút PHẦN CỨNG.
+KHÔNG CÓ ĐẢM BẢO bất kỳ nút nào trong số này sẽ hoạt động và ứng dụng này KHÔNG được thiết kế để điều khiển trò chơi. OEM/nhà cung cấp thiết bị của bạn có thể ngăn không cho chúng được ánh xạ lại.
+
+Bạn có thể kết hợp nhiều phím từ một thiết bị cụ thể hoặc bất kỳ thiết bị nào để tạo thành một "trình kích hoạt". Mỗi trigger có thể có nhiều hành động. Các phím có thể được cài đặt để nhấn cùng lúc hoặc lần lượt theo trình tự. Các phím có thể được ánh xạ lại khi chúng được nhấn nhanh, nhấn lâu hoặc nhấn đúp. Sơ đồ bàn phím có thể có một tập hợp các "ràng buộc" nên nó chỉ có tác dụng trong một số trường hợp nhất định.
+
+Những gì không thể được ánh xạ lại?
+ * Nút chuột
+ * Dpad, gậy ngón tay cái hoặc trình kích hoạt trên bộ điều khiển trò chơi
+
+Your key maps do not work if the screen is OFF. Đây là một hạn chế trong Android. Dev không thể làm gì được.
+
+Tôi có thể sắp xếp lại chìa khóa của mình để làm gì?
+Một số hành động sẽ chỉ hoạt động trên các thiết bị đã root và các phiên bản Android cụ thể.
+
+Có quá nhiều tính năng để liệt kê ở đây, vì vậy hãy xem danh sách đầy đủ tại đây: https://docs.keymapper.club/user-guide/actions
+
+Quyền
+Bạn không cần phải cấp tất cả các quyền để ứng dụng hoạt động. Ứng dụng sẽ cho bạn biết liệu có cần cấp quyền để một tính năng hoạt động hay không.
+
+ * Dịch vụ trợ năng: Yêu cầu cơ bản để ánh xạ lại hoạt động. It is needed so the app can listen to and block key events.
+ * Quản trị thiết bị: Để tắt màn hình khi sử dụng thao tác tắt màn hình.
+ * Sửa đổi cài đặt hệ thống: Để thay đổi cài đặt độ sáng và xoay.
+ * Camera: Để điều khiển đèn pin.
+
+ Trên một số thiết bị, việc bật dịch vụ trợ năng sẽ vô hiệu hóa "mã hóa dữ liệu nâng cao".
+
+Discord: www.keymapper.club
+Website: docs.keymapper.club
\ No newline at end of file
diff --git a/fastlane/metadata/android/vi_VN/short_description.txt b/fastlane/metadata/android/vi_VN/short_description.txt
new file mode 100644
index 0000000000..725018974c
--- /dev/null
+++ b/fastlane/metadata/android/vi_VN/short_description.txt
@@ -0,0 +1 @@
+Giải phóng chìa khóa của bạn! Nguồn mở!
\ No newline at end of file
diff --git a/fastlane/metadata/android/vi_VN/title.txt b/fastlane/metadata/android/vi_VN/title.txt
new file mode 100644
index 0000000000..19f819ebd7
--- /dev/null
+++ b/fastlane/metadata/android/vi_VN/title.txt
@@ -0,0 +1 @@
+Key Mapper
\ No newline at end of file
diff --git a/fastlane/metadata/android/zh_CN/full_description.txt b/fastlane/metadata/android/zh_CN/full_description.txt
index 31d83ace6d..7d61ff0bb3 100644
--- a/fastlane/metadata/android/zh_CN/full_description.txt
+++ b/fastlane/metadata/android/zh_CN/full_description.txt
@@ -1,9 +1,9 @@
什么能被重新映射?
- * 在支持的设备上的指纹手势。
* 音量键。
- * 导航按钮。
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* 蓝牙/有线键盘。
+ * 在支持的设备上的指纹手势。
* 其他已连接的设备的按钮也应该起作用。
只有实体按键能重新映射。
@@ -12,12 +12,10 @@
你可以组合来自特定设备或多个设备的多个按键来构成“触发器”。 每个触发器都可以有多个动作。 可以将按键设置为同时按下或依次按下。 按键可以在它们被短按、长按或双击时被重新映射。 按键映射可以有一组“约束,因此它仅在某些情况下有效。
什么不可以重新映射?
- * 电源键
- * Bixby键
* 鼠标键
* 游戏手柄上的Dpad、类比摇杆和触发器
-如果屏幕关闭,你的按键映射将不起作用。 这是Android中的一个限制。 开发者无能为力。
+Your key maps do not work if the screen is OFF. 这是Android中的一个限制。 开发者无能为力。
我重新映射我的按键能些做什么?
某些动作只在已root的设备和特定的Android版本上起作用。
@@ -27,7 +25,7 @@
权限
你无需授予所有权限来让这个应用工作。 如果一个功能需要授予权限才能工作,这个应用将告诉你。
- * 无障碍服务: 让重新映射工作的基本要求。 应用需要它才能监听和阻止按键事件。
+ * 无障碍服务: 让重新映射工作的基本要求。 It is needed so the app can listen to and block key events.
* 设备管理员: 用于在使用关闭屏幕的动作时关闭屏幕。
* 修改系统设置: 用于更改亮度和旋转设置。
* 相机: 用于控制闪光灯。
diff --git a/fastlane/metadata/android/zh_TW/full_description.txt b/fastlane/metadata/android/zh_TW/full_description.txt
index 515ccecd74..13f13d37dd 100644
--- a/fastlane/metadata/android/zh_TW/full_description.txt
+++ b/fastlane/metadata/android/zh_TW/full_description.txt
@@ -1,9 +1,9 @@
What can be remapped?
- * Fingerprint gestures on supported devices.
* Volume buttons.
- * Navigation buttons.
+ * Power button via Google Assistant, Pixel Active Edge, Double press Bixby button.
* Bluetooth/wired keyboards.
+ * Fingerprint gestures on supported devices.
* Buttons on other connected devices should also work.
ONLY HARDWARE buttons can be remapped.
@@ -12,12 +12,10 @@ There is NO GUARANTEE any of these buttons will work and this app is NOT designe
You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations.
What can’t be remapped?
- * Power button
- * Bixby button
* Mouse buttons
* Dpad, thumb sticks or triggers on game controllers
-Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
+Your key maps do not work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do.
What can I remap my keys to do?
Some actions will only work on rooted devices and specific Android versions.
@@ -27,7 +25,7 @@ There are too many features to list here so check out the full list here: https:
Permissions
You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work.
- * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents.
+ * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block key events.
* Device Admin: To turn the screen off when using the action to turn off the screen.
* Modify System Settings: To change the brightness and rotation settings.
* Camera: To control the flashlight.
diff --git a/mkdocs.yml b/mkdocs.yml
index e6defe3138..d81bdb28c4 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -94,7 +94,7 @@ markdown_extensions:
plugins:
- redirects:
redirect_maps:
- 'redirects/trigger-by-intent.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/keymaps#trigger-from-other-apps-230'
+ 'redirects/trigger-by-intent.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/keymaps#allow-other-apps-to-trigger-this-key-map-230'
'redirects/grant-write-secure-settings.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/adb-permissions#write_secure_settings'
'redirects/faq.md': 'https://keymapperorg.github.io/KeyMapper/faq'
'redirects/quick-start.md': 'https://keymapperorg.github.io/KeyMapper/quick-start'
@@ -104,14 +104,15 @@ plugins:
'redirects/trigger.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/keymaps#trigger'
'redirects/trigger-options.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/keymaps#special-options'
'redirects/keymap-action-options.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/keymaps#customising-actions'
- 'redirects/fingerprint-action-options.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/fingerprint-gestures#customising-fingerprint-gesture-maps'
+ 'redirects/fingerprint-action-options.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/fingerprint-gestures#customising-actions'
'redirects/trigger-key-options.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/keymaps#key-options'
- 'redirects/android-11-device-id-bug-work-around.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/settings#workaround-for-android-11-bug-that-sets-the-device-id-for-input-events-to-1-230-android-11'
+ 'redirects/android-11-device-id-bug-work-around.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/settings#fix-keyboards-that-are-set-to-us-english-230-android-11'
'redirects/settings.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/settings'
'redirects/report-issues.md': 'https://keymapperorg.github.io/KeyMapper/report-issues'
- 'redirects/fix-optimisation.md': 'https://keymapperorg.github.io/KeyMapper/known-issues.html#key-mapper-randomly-stops'
+ 'redirects/fix-optimisation.md': 'https://keymapperorg.github.io/KeyMapper/faq/#key-mapper-keeps-randomly-stoppingcrashingbuggingfreezing'
'redirects/shizuku-benefits.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/shizuku#benefits'
'redirects/cant-find-accessibility-settings.md': 'https://keymapperorg.github.io/KeyMapper/known-issues#key-mapper-cant-open-the-accessibility-settings-on-some-devices'
+ 'redirects/advanced-triggers.md': 'https://keymapperorg.github.io/KeyMapper/user-guide/keymaps#advanced-triggers'
- search:
lang:
- en
diff --git a/settings.gradle b/settings.gradle
index 41ccca99a4..a2a582b50a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,2 @@
include ':app'
include ':systemstubs'
-include ':pro'
diff --git a/systemstubs/build.gradle.kts b/systemstubs/build.gradle.kts
index 7121b0ba36..55b3f6ce1a 100644
--- a/systemstubs/build.gradle.kts
+++ b/systemstubs/build.gradle.kts
@@ -22,6 +22,9 @@ android {
"proguard-rules.pro",
)
}
+
+ create("debug_release") {
+ }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8