diff --git a/.cursor/rules/create-action.mdc b/.cursor/rules/create-action.mdc new file mode 100644 index 0000000000..e8b352f9db --- /dev/null +++ b/.cursor/rules/create-action.mdc @@ -0,0 +1,27 @@ +--- +description: +globs: +alwaysApply: false +--- +[ActionEntity.kt](mdc:app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt) [ActionDataEntityMapper.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt) [ActionData.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt) [ActionId.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt) [PerformActionsUseCase.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt) [ActionUtils.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt) [strings.xml](mdc:app/src/main/res/values/strings.xml) [ActionUiHelper.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt) [CreateActionDelegate.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt) + + +When you create an action you must follow these steps: + +0. Ask me whether the action is editable. +1. Create a new id in ActionId +2. Create a new ActionData +3. Map the data to and from an entity in ActionDataEntityMapper +4. Give the action a category in ActionUtils +5. If the action is editable then add it to the isEditable function in ActionUtils +6. Create a title for the action in strings.xml +7. Give the action a title and icon in ActionUtils. Only create a compose Icon. Ignore the drawable one. +8. Give the action a title in ActionUiHelper +9. Stub out the action in PerformActionsUseCase +10. Handle creating the action in CreateActionDelegate + +Important things to remember: + +- Do not delete any existing code for other actions. +- Follow the naming of existing code and strings and do not change them. +- Add code near existing code for similar actions. \ No newline at end of file diff --git a/.cursor/rules/jetpack-compose.mdc b/.cursor/rules/jetpack-compose.mdc new file mode 100644 index 0000000000..ef3b89eb3a --- /dev/null +++ b/.cursor/rules/jetpack-compose.mdc @@ -0,0 +1,71 @@ +--- +description: +globs: +alwaysApply: false +--- + +# Android Native (Jetpack Compose) + +# Android Jetpack Compose .cursorrules + +## Flexibility Notice +**Note:** This is a recommended project structure, but be flexible and adapt to existing project structures. Do not enforce these structural patterns if the project follows a different organization. Focus on maintaining consistency with the existing project architecture while applying Jetpack Compose best practices. + +## Project Architecture and Best Practices +```kotlin +val androidJetpackComposeBestPractices = listOf( + "Adapt to existing project architecture while maintaining clean code principles", + "Follow Material Design 3 guidelines and components", + "Implement clean architecture with domain, data, and presentation layers", + "Use Kotlin coroutines and Flow for asynchronous operations", + "Implement dependency injection using Hilt", + "Follow unidirectional data flow with ViewModel and UI State", + "Use Compose navigation for screen management", + "Implement proper state hoisting and composition" +) +``` + +## Folder Structure +**Note:** This is a reference structure. Adapt to the project's existing organization. +```plaintext +app/ + src/ + main/ + java/io/github/sds100/keymapper + actions/ + constraints/ + groups/ + home/ + mappings/ + keymaps/ + utils/ + res/ + values/ + drawable/ + mipmap/ + test/ + androidTest/ +``` + +## Compose UI Guidelines +1. Use `remember` and `derivedStateOf` appropriately +2. Implement proper recomposition optimization +3. Use proper Compose modifiers ordering +4. Follow composable function naming conventions +5. Implement proper preview annotations +6. Use proper state management with `MutableState` +7. Implement proper error handling and loading states +8. Use proper theming with `MaterialTheme` +9. Follow accessibility guidelines +10. Implement proper animation patterns + +## Performance Guidelines +1. Minimize recomposition using proper keys +2. Use proper lazy loading with `LazyColumn` and `LazyRow` +3. Implement efficient image loading +4. Use proper state management to prevent unnecessary updates +5. Follow proper lifecycle awareness +6. Implement proper memory management +7. Use proper background processing + + diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 9b58552057..17554251a8 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -5,6 +5,41 @@ + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index c585e6f557..be4801e662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,58 @@ -## [3.1.0](https://github.com/sds100/KeyMapper/releases/tag/v3.1.0) +## [3.1.2](https://github.com/sds100/KeyMapper/releases/tag/v3.1.2) #### TO BE RELEASED ## Added +- #1466 show onboarding when creating a key map for the first time + +## Changed + +- #1701 improve the order of the actions and categories + +## Bug fixes + +- #1686 (more fixes) do not show some screens behind system bars on the left/right side of the device. +- #1701 optimize the trigger screen for smaller screens so elements are less cut off. +- #1699 Do not highlight a floating button as if it is pressed after triggering a key event action from it. + +## [3.1.1](https://github.com/sds100/KeyMapper/releases/tag/v3.1.1) + +#### 12 May 2025 + +## Added + +- #1637 show a home screen error if notification permission is not granted. +- #1435 Pick system sounds/ringtones for the Sound action. + +## Bug fixes + +- Do not automatically select the key mapper keyboard when the accessibility service starts. +- #1686 do not show some screens behind system bars on the left/right side of the device. +- Use same sized list items when choosing a constraint. + +## [3.1.0](https://github.com/sds100/KeyMapper/releases/tag/v3.1.0) + +#### 10 May 2025 + +## Added + - #699 Time constraints ⏰ - #257 Action to interact with user interface elements inside other apps. +- #1663 Actions to stop, step forward, and step backward playing media. +- #1682 Show "Purchased!" text next to the use button for advanced triggers. ## Changed - Rename tap screen actions inside key maps. +## Bug fixes + +- #1683 key event actions work in Minecraft and other apps again. +- Export log files as .txt instead of .zip files. +- #1684 Removed the redundant and broken refresh devices button when configuring a key event action because they are automatically refreshed anyway. +- #1687 restoring key map groups would sometimes fail. + ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) #### 28 April 2025 diff --git a/CREDITS.md b/CREDITS.md index 2327ac943f..24c12e44f6 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -8,11 +8,12 @@ Many thanks to... - [App Mockup] for their screenshot utility. - Airbnb for their [Epoxy](https://github.com/airbnb/epoxy) RecyclerView. They made having multiple itemview types and dragging and dropping super easy! - @[Jake Wharton](https://github.com/JakeWharton) for his [Timber](https://github.com/JakeWharton/timber) logging library. - - @[AppIntro](https://github.com/AppIntro) for their [AppIntro](https://github.com/AppIntro/AppIntro) library. - @[srikanth-lingala](https://github.com/srikanth-lingala) for their Java zip file [library](https://github.com/srikanth-lingala/zip4j). - @[anggrayudi](https://github.com/anggrayudi) for their [Simple Storage](https://github.com/anggrayudi/SimpleStorage) library that makes working with Scoped Storage and the Storage Access Framework on Android much easier. - @[MFlisar](https://github.com/MFlisar) for their [drag and select](https://github.com/MFlisar/DragSelectRecyclerView) library. - @[RikkaApps](https://github.com/RikkaApps) for Shizuku! It is amazing. + - @[canopas](https://github.com/canopas) for their Jetpack Compose Tap Target library https://github.com/canopas/compose-intro-showcase. + - @[topjohnwu](https://github.com/topjohnwu) for Magisk and [libsu](https://github.com/topjohnwu/libsu). [salomonbrys]: https://github.com/salomonbrys [Kotson]: https://github.com/salomonbrys/Kotson diff --git a/app/build.gradle b/app/build.gradle index e442585f81..298cf2d825 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,12 +162,14 @@ android { dependencies { implementation fileTree(include: ["*.jar"], dir: "libs") + implementation project(':nativelib') + implementation project(':privstarter') compileOnly project(":systemstubs") def room_version = "2.7.1" def coroutinesVersion = "1.9.0" - def nav_version = '2.8.9' + def nav_version = '2.9.0' def epoxy_version = "4.6.2" def splitties_version = "3.0.0" def multidex_version = "2.0.1" @@ -192,10 +194,12 @@ dependencies { implementation "dev.rikka.shizuku:api:$shizuku_version" implementation "dev.rikka.shizuku:provider:$shizuku_version" implementation "org.lsposed.hiddenapibypass:hiddenapibypass:4.3" - proImplementation 'com.revenuecat.purchases:purchases:8.17.0' + proImplementation 'com.revenuecat.purchases:purchases:8.17.1' proImplementation "com.airbnb.android:lottie-compose:6.6.3" implementation("com.squareup.okhttp3:okhttp:4.12.0") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") + implementation 'com.canopas.intro-showcase-view:introshowcaseview:2.0.2' + implementation "com.github.topjohnwu.libsu:core:6.0.0" // splitties implementation "com.louiscad.splitties:splitties-bitflags:$splitties_version" @@ -211,9 +215,9 @@ dependencies { implementation "androidx.activity:activity-ktx:1.10.1" implementation "androidx.fragment:fragment-ktx:1.8.6" - 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.lifecycle:lifecycle-viewmodel-ktx:2.9.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.9.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.9.0" implementation "androidx.room:room-ktx:$room_version" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" @@ -225,15 +229,15 @@ dependencies { implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.viewpager2:viewpager2:1.1.0" - implementation "androidx.datastore:datastore-preferences:1.2.0-alpha01" + implementation "androidx.datastore:datastore-preferences:1.2.0-alpha02" implementation "androidx.core:core-splashscreen:1.0.1" implementation "androidx.activity:activity-compose:1.10.1" - implementation "androidx.navigation:navigation-compose:2.8.9" - implementation "androidx.navigation:navigation-fragment-compose:2.8.9" + implementation "androidx.navigation:navigation-compose:2.9.0" + implementation "androidx.navigation:navigation-fragment-compose:2.9.0" ksp "androidx.room:room-compiler:$room_version" // Compose - Dependency composeBom = platform('androidx.compose:compose-bom-beta:2025.04.01') + Dependency composeBom = platform('androidx.compose:compose-bom-beta:2025.05.00') implementation composeBom implementation 'androidx.compose.foundation:foundation' implementation "androidx.compose.ui:ui-android" diff --git a/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/20.json b/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/20.json new file mode 100644 index 0000000000..3ee2ff236a --- /dev/null +++ b/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/20.json @@ -0,0 +1,460 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "f2f5eac59b7bdee472c0dd7ff9bae4b2", + "entities": [ + { + "tableName": "keymaps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trigger` TEXT NOT NULL, `action_list` TEXT NOT NULL, `constraint_list` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `is_enabled` INTEGER NOT NULL, `uid` TEXT NOT NULL, `group_uid` TEXT, FOREIGN KEY(`group_uid`) REFERENCES `groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trigger", + "columnName": "trigger", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actionList", + "columnName": "action_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraint_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "groupUid", + "columnName": "group_uid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_keymaps_uid", + "unique": true, + "columnNames": [ + "uid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_keymaps_uid` ON `${TABLE_NAME}` (`uid`)" + } + ], + "foreignKeys": [ + { + "table": "groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "group_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "fingerprintmaps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `action_list` TEXT NOT NULL, `constraint_list` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `extras` TEXT NOT NULL, `flags` INTEGER NOT NULL, `is_enabled` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actionList", + "columnName": "action_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraint_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `severity` INTEGER NOT NULL, `message` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "severity", + "columnName": "severity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "floating_layouts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`uid`))", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_floating_layouts_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_floating_layouts_name` ON `${TABLE_NAME}` (`name`)" + } + ] + }, + { + "tableName": "floating_buttons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `layout_uid` TEXT NOT NULL, `text` TEXT NOT NULL, `button_size` INTEGER NOT NULL, `x` INTEGER NOT NULL, `y` INTEGER NOT NULL, `orientation` TEXT NOT NULL, `display_width` INTEGER NOT NULL, `display_height` INTEGER NOT NULL, `border_opacity` REAL, `background_opacity` REAL, PRIMARY KEY(`uid`), FOREIGN KEY(`layout_uid`) REFERENCES `floating_layouts`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layoutUid", + "columnName": "layout_uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "buttonSize", + "columnName": "button_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "x", + "columnName": "x", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "y", + "columnName": "y", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orientation", + "columnName": "orientation", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayWidth", + "columnName": "display_width", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayHeight", + "columnName": "display_height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "borderOpacity", + "columnName": "border_opacity", + "affinity": "REAL" + }, + { + "fieldPath": "backgroundOpacity", + "columnName": "background_opacity", + "affinity": "REAL" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_floating_buttons_layout_uid", + "unique": false, + "columnNames": [ + "layout_uid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_floating_buttons_layout_uid` ON `${TABLE_NAME}` (`layout_uid`)" + } + ], + "foreignKeys": [ + { + "table": "floating_layouts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "layout_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `name` TEXT NOT NULL, `constraints` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `parent_uid` TEXT, `last_opened_date` INTEGER, PRIMARY KEY(`uid`), FOREIGN KEY(`parent_uid`) REFERENCES `groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraints", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentUid", + "columnName": "parent_uid", + "affinity": "TEXT" + }, + { + "fieldPath": "lastOpenedDate", + "columnName": "last_opened_date", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "foreignKeys": [ + { + "table": "groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "accessibility_nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_name` TEXT NOT NULL, `text` TEXT, `content_description` TEXT, `class_name` TEXT, `view_resource_id` TEXT, `unique_id` TEXT, `actions` INTEGER NOT NULL, `interacted` INTEGER NOT NULL DEFAULT false, `tooltip` TEXT DEFAULT NULL, `hint` TEXT DEFAULT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT" + }, + { + "fieldPath": "contentDescription", + "columnName": "content_description", + "affinity": "TEXT" + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT" + }, + { + "fieldPath": "viewResourceId", + "columnName": "view_resource_id", + "affinity": "TEXT" + }, + { + "fieldPath": "uniqueId", + "columnName": "unique_id", + "affinity": "TEXT" + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interacted", + "columnName": "interacted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "tooltip", + "columnName": "tooltip", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "hint", + "columnName": "hint", + "affinity": "TEXT", + "defaultValue": "NULL" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2f5eac59b7bdee472c0dd7ff9bae4b2')" + ] + } +} \ No newline at end of file diff --git a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt b/app/src/free/java/io/github/sds100/keymapper/keymaps/detection/KeyMapController.kt similarity index 98% rename from app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt rename to app/src/free/java/io/github/sds100/keymapper/keymaps/detection/KeyMapController.kt index 1b0703f604..5857156523 100644 --- a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt +++ b/app/src/free/java/io/github/sds100/keymapper/keymaps/detection/KeyMapController.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.detection +package io.github.sds100.keymapper.keymaps.detection import android.view.KeyEvent import androidx.collection.SparseArrayCompat @@ -13,18 +13,18 @@ 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.FingerprintGestureType -import io.github.sds100.keymapper.mappings.keymaps.trigger.FingerprintTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource -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 +import io.github.sds100.keymapper.keymaps.ClickType +import io.github.sds100.keymapper.keymaps.FingerprintGestureType import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.system.inputevents.MyKeyEvent import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.trigger.FingerprintTriggerKey +import io.github.sds100.keymapper.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.trigger.Trigger +import io.github.sds100.keymapper.trigger.TriggerKey +import io.github.sds100.keymapper.trigger.TriggerKeyDevice +import io.github.sds100.keymapper.trigger.TriggerMode import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.Result 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 deleted file mode 100644 index 3d6731ccdf..0000000000 --- a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AdvancedTriggersBottomSheet.kt +++ /dev/null @@ -1,145 +0,0 @@ -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, - // Hide drag handle because other bottom sheets don't have it - dragHandle = {}, - ) { - Spacer(modifier = Modifier.height(8.dp)) - - 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/system/accessibility/AccessibilityServiceController.kt b/app/src/free/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 8e0fb9dda5..44c0fcc963 100644 --- 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 @@ -4,9 +4,9 @@ import io.github.sds100.keymapper.actions.PerformActionsUseCase import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.mappings.FingerprintGesturesSupportedUseCase -import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCase -import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.FingerprintGesturesSupportedUseCase +import io.github.sds100.keymapper.keymaps.PauseKeyMapsUseCase +import io.github.sds100.keymapper.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 diff --git a/app/src/free/java/io/github/sds100/keymapper/trigger/AdvancedTriggersBottomSheet.kt b/app/src/free/java/io/github/sds100/keymapper/trigger/AdvancedTriggersBottomSheet.kt new file mode 100644 index 0000000000..dc1abbe1d9 --- /dev/null +++ b/app/src/free/java/io/github/sds100/keymapper/trigger/AdvancedTriggersBottomSheet.kt @@ -0,0 +1,155 @@ +package io.github.sds100.keymapper.trigger + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.Alignment +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, + // Hide drag handle because other bottom sheets don't have it + dragHandle = {}, + ) { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Spacer(modifier = Modifier.height(8.dp)) + + 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, + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedButton( + modifier = Modifier, + onClick = { + scope.launch { + sheetState.hide() + onDismissRequest() + } + }, + ) { + Text(stringResource(R.string.neg_cancel)) + } + + Spacer(modifier = Modifier.width(8.dp)) + + 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/AssistantTriggerSetupBottomSheet.kt b/app/src/free/java/io/github/sds100/keymapper/trigger/AssistantTriggerSetupBottomSheet.kt similarity index 70% rename from app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerSetupBottomSheet.kt rename to app/src/free/java/io/github/sds100/keymapper/trigger/AssistantTriggerSetupBottomSheet.kt index 3730a02096..8c3c7cbd59 100644 --- a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerSetupBottomSheet.kt +++ b/app/src/free/java/io/github/sds100/keymapper/trigger/AssistantTriggerSetupBottomSheet.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import androidx.compose.runtime.Composable 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/trigger/ConfigTriggerViewModel.kt similarity index 74% rename from app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt rename to app/src/free/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt index cb30491821..89a8da1b07 100644 --- a/app/src/free/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ConfigTriggerViewModel.kt +++ b/app/src/free/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt @@ -1,9 +1,9 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger -import io.github.sds100.keymapper.mappings.FingerprintGesturesSupportedUseCase -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.keymaps.ConfigKeyMapUseCase +import io.github.sds100.keymapper.keymaps.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.keymaps.DisplayKeyMapUseCase +import io.github.sds100.keymapper.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.purchasing.PurchasingManager import io.github.sds100.keymapper.util.ui.ResourceProvider diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c19777221d..20fe0052df 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,6 +22,8 @@ + + @@ -126,7 +128,7 @@ @@ -321,5 +323,12 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> + + + \ No newline at end of file diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt index 7c29bd65d8..aa0d3355cb 100644 --- a/app/src/main/assets/whats-new.txt +++ b/app/src/main/assets/whats-new.txt @@ -1,4 +1,12 @@ -Key Mapper 3.0 is here! 🎉 +Fix for Minecraft 1.21.80! + +⏰ Time constraints. + +🔎 Action to interact with app elements. + +Other bug fixes. + +== 3.0 features == 🫧 Floating Buttons: you can create custom on-screen buttons to trigger key maps. 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 8fefdddbe4..fc7f019271 100644 --- a/app/src/main/java/io/github/sds100/keymapper/ActivityViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/ActivityViewModel.kt @@ -1,5 +1,8 @@ package io.github.sds100.keymapper +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -9,7 +12,14 @@ import io.github.sds100.keymapper.util.ui.PopupViewModel import io.github.sds100.keymapper.util.ui.PopupViewModelImpl import io.github.sds100.keymapper.util.ui.ResourceProvider import io.github.sds100.keymapper.util.ui.ViewModelHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import moe.shizuku.manager.adb.AdbClient +import moe.shizuku.manager.adb.AdbKey +import moe.shizuku.manager.adb.AdbKeyException +import moe.shizuku.manager.adb.PreferenceAdbKeyStore +import moe.shizuku.manager.starter.Starter /** * Created by sds100 on 23/07/2021. @@ -38,7 +48,6 @@ class ActivityViewModel( private val resourceProvider: ResourceProvider, ) : ViewModelProvider.NewInstanceFactory() { - override fun create(modelClass: Class): T = - ActivityViewModel(resourceProvider) as T + override fun create(modelClass: Class): T = ActivityViewModel(resourceProvider) as T } } diff --git a/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt b/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt index 98cf6232c0..667dd55bd8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt @@ -1,21 +1,34 @@ package io.github.sds100.keymapper +import android.app.AppOpsManager +import android.app.ForegroundServiceStartNotAllowedException import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.ServiceConnection import android.content.res.Configuration +import android.graphics.Typeface import android.net.Uri +import android.os.Build import android.os.Bundle +import android.os.IBinder +import android.provider.Settings +import android.util.Log import android.view.MotionEvent +import android.widget.TextView +import android.widget.Toast import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.viewModels +import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.ui.graphics.toArgb import androidx.core.content.ContextCompat import androidx.core.content.IntentCompat +import androidx.core.os.bundleOf import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.databinding.DataBindingUtil import androidx.lifecycle.Lifecycle @@ -23,24 +36,36 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.withStateAtLeast import androidx.navigation.findNavController +import androidx.preference.PreferenceManager import com.anggrayudi.storage.extension.openInputStream import com.anggrayudi.storage.extension.openOutputStream import com.anggrayudi.storage.extension.toDocumentFile +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.github.sds100.keymapper.Constants.PACKAGE_NAME import io.github.sds100.keymapper.compose.ComposeColors import io.github.sds100.keymapper.databinding.ActivityMainBinding -import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerController +import io.github.sds100.keymapper.nativelib.IEvdevService +import io.github.sds100.keymapper.nativelib.adb.AdbPairingService import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.files.FileUtils import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.RequestPermissionDelegate +import io.github.sds100.keymapper.system.root.SuAdapterImpl +import io.github.sds100.keymapper.trigger.RecordTriggerController import io.github.sds100.keymapper.util.launchRepeatOnLifecycle import io.github.sds100.keymapper.util.ui.showPopups import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import moe.shizuku.manager.adb.AdbClient +import moe.shizuku.manager.adb.AdbKey +import moe.shizuku.manager.adb.AdbKeyException +import moe.shizuku.manager.adb.PreferenceAdbKeyStore +import moe.shizuku.manager.starter.Starter import timber.log.Timber /** @@ -68,6 +93,10 @@ abstract class BaseMainActivity : AppCompatActivity() { ServiceLocator.accessibilityServiceAdapter(this) } + private val suAdapter: SuAdapterImpl by lazy { + ServiceLocator.suAdapter(this) + } + val viewModel by viewModels { ActivityViewModel.Factory(ServiceLocator.resourceProvider(this)) } @@ -109,6 +138,19 @@ abstract class BaseMainActivity : AppCompatActivity() { } } + private val evdevServiceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(componentName: ComponentName, binder: IBinder) { + val service = IEvdevService.Stub.asInterface(binder) + + lifecycleScope.launch(Dispatchers.Default) { + Timber.e("RECEIVED FROM EVDEV ${service.sendEvent()}") + } + } + + override fun onServiceDisconnected(componentName: ComponentName) { + } + } + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() enableEdgeToEdge( @@ -169,6 +211,65 @@ abstract class BaseMainActivity : AppCompatActivity() { ContextCompat.RECEIVER_EXPORTED, ) } + + // Go to build number. +// Intent(Settings.ACTION_DEVICE_INFO_SETTINGS).apply { +// val EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key" +// val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" +// +// putExtra(EXTRA_FRAGMENT_ARG_KEY, "build_number") +// val bundle = bundleOf(EXTRA_FRAGMENT_ARG_KEY to "build_number") +// putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle) +// +// startActivity(this) +// } + + // Highlight wireless debugging setting + Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply { + // See SubSettingLauncher in the Android Settings code for how to + // launch a specific fragment. + // https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/core/SubSettingLauncher.java +// val EXTRA_SHOW_FRAGMENT = ":settings:show_fragment" +// putExtra(EXTRA_SHOW_FRAGMENT, "com.android.settings.development.WirelessDebuggingFragment") +// putExtra(":settings:source_metrics", 1831) + + val EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key" + val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" + + putExtra(EXTRA_FRAGMENT_ARG_KEY, "toggle_adb_wireless") + val bundle = bundleOf(EXTRA_FRAGMENT_ARG_KEY to "toggle_adb_wireless") + putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle) +// startActivity(this) + } + + // See demo.DemoActivity in the Shizuku-API repository. + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { +// startPairingService() + startAdb("127.0.0.1", 33697) + } + +// val userServiceArgs = +// UserServiceArgs( +// ComponentName( +// BuildConfig.APPLICATION_ID, +// EvdevService::class.java.getName(), +// ), +// ) +// .daemon(false) +// .processNameSuffix("service") +// .debuggable(BuildConfig.DEBUG) +// .version(BuildConfig.VERSION_CODE) +// +// try { +// if (Shizuku.getVersion() < 10) { +// Timber.e("requires Shizuku API 10") +// } else { +// Shizuku.bindUserService(userServiceArgs, evdevServiceConnection) +// } +// } catch (tr: Throwable) { +// tr.printStackTrace() +// } } override fun onResume() { @@ -182,6 +283,7 @@ abstract class BaseMainActivity : AppCompatActivity() { // the activities have not necessarily resumed at that point. permissionAdapter.onPermissionsChanged() serviceAdapter.updateWhetherServiceIsEnabled() + suAdapter.invalidateIsRooted() } override fun onDestroy() { @@ -225,4 +327,136 @@ abstract class BaseMainActivity : AppCompatActivity() { originalFileUri = fileUri saveFileLauncher.launch(fileName) } + + private val sb = StringBuilder() + + fun postResult(throwable: Throwable? = null) { + if (throwable == null) { + Timber.e(sb.toString()) + } else { + Timber.e(throwable) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun startPairingService() { + val intent = AdbPairingService.startIntent(this) + try { + startForegroundService(intent) + } catch (e: Throwable) { + Timber.e("startForegroundService", e) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + e is ForegroundServiceStartNotAllowedException + ) { + val mode = getSystemService(AppOpsManager::class.java) + .noteOpNoThrow( + "android:start_foreground", + android.os.Process.myUid(), + packageName, + null, + null, + ) + if (mode == AppOpsManager.MODE_ERRORED) { + Toast.makeText( + this, + "OP_START_FOREGROUND is denied. What are you doing?", + Toast.LENGTH_LONG, + ).show() + } + startService(intent) + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun writeStarterFiles() { + lifecycleScope.launch(Dispatchers.IO) { + try { + Starter.writeSdcardFiles(applicationContext) + } catch (e: Throwable) { + withContext(Dispatchers.Main) { + MaterialAlertDialogBuilder(this@BaseMainActivity) + .setTitle("Cannot write files") + .setMessage(Log.getStackTraceString(e)) + .setPositiveButton(android.R.string.ok, null) + .create() + .apply { + setOnShowListener { + this.findViewById(android.R.id.message)!!.apply { + typeface = Typeface.MONOSPACE + setTextIsSelectable(true) + } + } + } + .show() + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun startAdb(host: String, port: Int) { + writeStarterFiles() + + sb.append("Starting with wireless adb...").append('\n').append('\n') + postResult() + + GlobalScope.launch(Dispatchers.IO) { + val key = try { + AdbKey( + PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(this@BaseMainActivity)), + "shizuku", + ) + } catch (e: Throwable) { + e.printStackTrace() + sb.append('\n').append(Log.getStackTraceString(e)) + + postResult(AdbKeyException(e)) + return@launch + } + + AdbClient(host, port, key).runCatching { + connect() + shellCommand(Starter.sdcardCommand) { + sb.append(String(it)) + postResult() + } + close() + }.onFailure { + it.printStackTrace() + + sb.append('\n').append(Log.getStackTraceString(it)) + postResult(it) + } + + /* Adb on MIUI Android 11 has no permission to access Android/data. + Before MIUI Android 12, we can temporarily use /data/user_de. + After that, is better to implement "adb push" and push files directly to /data/local/tmp. + */ + if (sb.contains("/Android/data/${BuildConfig.APPLICATION_ID}/start.sh: Permission denied")) { + sb.append('\n') + .appendLine("adb have no permission to access Android/data, how could this possible ?!") + .appendLine("try /data/user_de instead...") + .appendLine() + postResult() + + Starter.writeDataFiles(application, true) + + AdbClient(host, port, key).runCatching { + connect() + shellCommand(Starter.dataCommand) { + sb.append(String(it)) + postResult() + } + close() + }.onFailure { + it.printStackTrace() + + sb.append('\n').append(Log.getStackTraceString(it)) + postResult(it) + } + } + } + } } 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 162bbd5858..fdd0ad4952 100644 --- a/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt +++ b/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt @@ -16,7 +16,6 @@ import io.github.sds100.keymapper.actions.uielement.InteractUiElementController 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 @@ -50,10 +49,12 @@ import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.phone.AndroidPhoneAdapter import io.github.sds100.keymapper.system.popup.AndroidToastAdapter import io.github.sds100.keymapper.system.power.AndroidPowerAdapter +import io.github.sds100.keymapper.system.ringtones.AndroidRingtoneAdapter import io.github.sds100.keymapper.system.root.SuAdapterImpl import io.github.sds100.keymapper.system.url.AndroidOpenUrlAdapter import io.github.sds100.keymapper.system.vibrator.AndroidVibratorAdapter import io.github.sds100.keymapper.system.volume.AndroidVolumeAdapter +import io.github.sds100.keymapper.trigger.RecordTriggerController import io.github.sds100.keymapper.util.ui.ResourceProviderImpl import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.collectLatest @@ -129,12 +130,7 @@ class KeyMapperApp : MultiDexApplication() { val vibratorAdapter by lazy { AndroidVibratorAdapter(this) } val displayAdapter by lazy { AndroidDisplayAdapter(this, coroutineScope = appCoroutineScope) } val audioAdapter by lazy { AndroidVolumeAdapter(this) } - val suAdapter by lazy { - SuAdapterImpl( - appCoroutineScope, - ServiceLocator.settingsRepository(this), - ) - } + val suAdapter by lazy { SuAdapterImpl(appCoroutineScope) } val phoneAdapter by lazy { AndroidPhoneAdapter(this, appCoroutineScope) } val intentAdapter by lazy { IntentAdapterImpl(this) } val mediaAdapter by lazy { AndroidMediaAdapter(this, appCoroutineScope) } @@ -174,6 +170,10 @@ class KeyMapperApp : MultiDexApplication() { PurchasingManagerImpl(this.applicationContext, appCoroutineScope) } + val ringtoneManagerAdapter: AndroidRingtoneAdapter by lazy { + AndroidRingtoneAdapter(this) + } + private val loggingTree by lazy { KeyMapperLoggingTree( appCoroutineScope, 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 c0dd401af4..94816343ce 100755 --- a/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt @@ -20,8 +20,8 @@ import io.github.sds100.keymapper.data.repositories.RoomGroupRepository import io.github.sds100.keymapper.data.repositories.RoomKeyMapRepository import io.github.sds100.keymapper.data.repositories.RoomLogRepository import io.github.sds100.keymapper.data.repositories.SettingsPreferenceRepository +import io.github.sds100.keymapper.keymaps.ConfigKeyMapUseCaseController import io.github.sds100.keymapper.logging.LogRepository -import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCaseController import io.github.sds100.keymapper.purchasing.PurchasingManagerImpl import io.github.sds100.keymapper.shizuku.ShizukuAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter @@ -49,7 +49,8 @@ import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter import io.github.sds100.keymapper.system.phone.PhoneAdapter import io.github.sds100.keymapper.system.popup.PopupMessageAdapter import io.github.sds100.keymapper.system.power.PowerAdapter -import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter +import io.github.sds100.keymapper.system.root.SuAdapterImpl import io.github.sds100.keymapper.system.url.OpenUrlAdapter import io.github.sds100.keymapper.system.vibrator.VibratorAdapter import io.github.sds100.keymapper.system.volume.VolumeAdapter @@ -266,7 +267,7 @@ object ServiceLocator { fun audioAdapter(context: Context): VolumeAdapter = (context.applicationContext as KeyMapperApp).audioAdapter - fun suAdapter(context: Context): SuAdapter = (context.applicationContext as KeyMapperApp).suAdapter + fun suAdapter(context: Context): SuAdapterImpl = (context.applicationContext as KeyMapperApp).suAdapter fun intentAdapter(context: Context): IntentAdapter = (context.applicationContext as KeyMapperApp).intentAdapter @@ -296,6 +297,8 @@ object ServiceLocator { fun purchasingManager(context: Context): PurchasingManagerImpl = (context.applicationContext as KeyMapperApp).purchasingManager + fun ringtoneAdapter(context: Context): RingtoneAdapter = (context.applicationContext as KeyMapperApp).ringtoneManagerAdapter + private fun createDatabase(context: Context): AppDatabase = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, 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 caee6e48b0..ef7bbc2dd0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt +++ b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt @@ -9,19 +9,19 @@ import io.github.sds100.keymapper.constraints.DetectConstraintsUseCaseImpl import io.github.sds100.keymapper.constraints.GetConstraintErrorUseCaseImpl import io.github.sds100.keymapper.floating.ListFloatingLayoutsUseCase import io.github.sds100.keymapper.floating.ListFloatingLayoutsUseCaseImpl -import io.github.sds100.keymapper.mappings.FingerprintGesturesSupportedUseCaseImpl -import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCaseImpl -import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.mappings.keymaps.CreateKeyMapShortcutUseCaseImpl -import io.github.sds100.keymapper.mappings.keymaps.DisplayKeyMapUseCase -import io.github.sds100.keymapper.mappings.keymaps.DisplayKeyMapUseCaseImpl -import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.keymaps.ConfigKeyMapUseCase +import io.github.sds100.keymapper.keymaps.CreateKeyMapShortcutUseCaseImpl +import io.github.sds100.keymapper.keymaps.DisplayKeyMapUseCase +import io.github.sds100.keymapper.keymaps.DisplayKeyMapUseCaseImpl +import io.github.sds100.keymapper.keymaps.FingerprintGesturesSupportedUseCaseImpl +import io.github.sds100.keymapper.keymaps.PauseKeyMapsUseCaseImpl +import io.github.sds100.keymapper.keymaps.detection.DetectKeyMapsUseCaseImpl import io.github.sds100.keymapper.onboarding.OnboardingUseCaseImpl import io.github.sds100.keymapper.reroutekeyevents.RerouteKeyEventsUseCaseImpl import io.github.sds100.keymapper.shizuku.ShizukuInputEventInjector import io.github.sds100.keymapper.sorting.SortKeyMapsUseCase import io.github.sds100.keymapper.sorting.SortKeyMapsUseCaseImpl -import io.github.sds100.keymapper.system.Shell +import io.github.sds100.keymapper.system.SimpleShell import io.github.sds100.keymapper.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.system.accessibility.ControlAccessibilityServiceUseCaseImpl import io.github.sds100.keymapper.system.accessibility.IAccessibilityService @@ -57,6 +57,7 @@ object UseCases { ServiceLocator.accessibilityServiceAdapter(ctx), ServiceLocator.settingsRepository(ctx), ServiceLocator.purchasingManager(ctx), + ServiceLocator.ringtoneAdapter(ctx), getActionError(ctx), getConstraintError(ctx), ) @@ -71,6 +72,7 @@ object UseCases { ServiceLocator.cameraAdapter(ctx), ServiceLocator.soundsManager(ctx), ServiceLocator.shizukuAdapter(ctx), + ServiceLocator.ringtoneAdapter(ctx), ) fun getConstraintError(ctx: Context) = GetConstraintErrorUseCaseImpl( @@ -88,6 +90,8 @@ object UseCases { ServiceLocator.shizukuAdapter(ctx), ServiceLocator.permissionAdapter(ctx), ServiceLocator.packageManagerAdapter(ctx), + ServiceLocator.purchasingManager(ctx), + ServiceLocator.roomKeyMapRepository(ctx), ) fun createKeymapShortcut(ctx: Context) = CreateKeyMapShortcutUseCaseImpl( @@ -100,6 +104,7 @@ object UseCases { fun pauseKeyMaps(ctx: Context) = PauseKeyMapsUseCaseImpl( ServiceLocator.settingsRepository(ctx), ServiceLocator.mediaAdapter(ctx), + ServiceLocator.ringtoneAdapter(ctx), ) fun showImePicker(ctx: Context): ShowInputMethodPickerUseCase = ShowInputMethodPickerUseCaseImpl( @@ -138,11 +143,11 @@ object UseCases { ServiceLocator.inputMethodAdapter(ctx), ServiceLocator.fileAdapter(ctx), ServiceLocator.suAdapter(ctx), - Shell, + SimpleShell, ServiceLocator.intentAdapter(ctx), getActionError(ctx), keyMapperImeMessenger(ctx, keyEventRelayService), - ShizukuInputEventInjector(coroutineScope = ServiceLocator.appCoroutineScope(ctx)), + ShizukuInputEventInjector(), ServiceLocator.packageManagerAdapter(ctx), ServiceLocator.appShortcutAdapter(ctx), ServiceLocator.popupMessageAdapter(ctx), @@ -163,6 +168,7 @@ object UseCases { ServiceLocator.soundsManager(ctx), ServiceLocator.permissionAdapter(ctx), ServiceLocator.notificationReceiverAdapter(ctx), + ServiceLocator.ringtoneAdapter(ctx), ) fun detectKeyMaps( @@ -179,7 +185,7 @@ object UseCases { ServiceLocator.audioAdapter(ctx), keyMapperImeMessenger(ctx, keyEventRelayService), service, - ShizukuInputEventInjector(ServiceLocator.appCoroutineScope(ctx)), + ShizukuInputEventInjector(), ServiceLocator.popupMessageAdapter(ctx), ServiceLocator.permissionAdapter(ctx), ServiceLocator.resourceProvider(ctx), diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/Action.kt b/app/src/main/java/io/github/sds100/keymapper/actions/Action.kt index df4eccb63b..e8e1050ea7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/Action.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/Action.kt @@ -3,7 +3,7 @@ package io.github.sds100.keymapper.actions import io.github.sds100.keymapper.data.entities.ActionEntity import io.github.sds100.keymapper.data.entities.EntityExtra import io.github.sds100.keymapper.data.entities.getData -import io.github.sds100.keymapper.mappings.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMap import io.github.sds100.keymapper.util.success import io.github.sds100.keymapper.util.then import io.github.sds100.keymapper.util.valueOrNull @@ -33,11 +33,7 @@ data class Action( val multiplier: Int? = null, val delayBeforeNextAction: Int? = null, -) { - companion object { - const val REPEAT_DELAY_MIN = 0 - } -} +) object ActionEntityMapper { fun fromEntity(entity: ActionEntity): Action? { @@ -108,7 +104,7 @@ object ActionEntityMapper { ) } - fun toEntity(keyMap: KeyMap): List = keyMap.actionList.mapNotNull { action -> + fun toEntity(keyMap: KeyMap): List = keyMap.actionList.map { action -> val base = ActionDataEntityMapper.toEntity(action.data) val extras = mutableListOf().apply { @@ -187,7 +183,7 @@ object ActionEntityMapper { flags = flags.withFlag(ActionEntity.ACTION_FLAG_HOLD_DOWN) } - return@mapNotNull ActionEntity( + return@map ActionEntity( type = base.type, data = base.data, extras = base.extras.plus(extras), diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionCategory.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionCategory.kt index 6de92e797a..c6dfed43a6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionCategory.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionCategory.kt @@ -6,7 +6,7 @@ package io.github.sds100.keymapper.actions enum class ActionCategory { APPS, INPUT, - CAMERA_SOUND, + FLASHLIGHT, CONNECTIVITY, CONTENT, NAVIGATION, @@ -17,4 +17,5 @@ enum class ActionCategory { INTERFACE, TELEPHONY, NOTIFICATIONS, + SPECIAL, } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index 3823a2e5f7..7889ee4f2a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -67,15 +67,32 @@ sealed class ActionData : Comparable { } @Serializable - data class Sound( - val soundUid: String, - val soundDescription: String, - ) : ActionData() { + sealed class Sound : ActionData() { override val id = ActionId.SOUND - override fun compareTo(other: ActionData) = when (other) { - is Sound -> soundUid.compareTo(other.soundUid) - else -> super.compareTo(other) + @Serializable + data class SoundFile( + val soundUid: String, + val soundDescription: String, + ) : Sound() { + override fun compareTo(other: ActionData): Int { + return when (other) { + is SoundFile -> soundUid.compareTo(other.soundUid) + else -> super.compareTo(other) + } + } + } + + @Serializable + data class Ringtone( + val uri: String, + ) : Sound() { + override fun compareTo(other: ActionData): Int { + return when (other) { + is Ringtone -> uri.compareTo(other.uri) + else -> super.compareTo(other) + } + } } } @@ -365,6 +382,21 @@ sealed class ActionData : Comparable { data class Rewind(override val packageName: String) : ControlMediaForApp() { override val id = ActionId.REWIND_PACKAGE } + + @Serializable + data class Stop(override val packageName: String) : ControlMediaForApp() { + override val id = ActionId.STOP_MEDIA_PACKAGE + } + + @Serializable + data class StepForward(override val packageName: String) : ControlMediaForApp() { + override val id = ActionId.STEP_FORWARD_PACKAGE + } + + @Serializable + data class StepBackward(override val packageName: String) : ControlMediaForApp() { + override val id = ActionId.STEP_BACKWARD_PACKAGE + } } @Serializable @@ -403,6 +435,21 @@ sealed class ActionData : Comparable { data object Rewind : ControlMedia() { override val id = ActionId.REWIND } + + @Serializable + data object Stop : ControlMedia() { + override val id = ActionId.STOP_MEDIA + } + + @Serializable + data object StepForward : ControlMedia() { + override val id = ActionId.STEP_FORWARD + } + + @Serializable + data object StepBackward : ControlMedia() { + override val id = ActionId.STEP_BACKWARD + } } @Serializable @@ -853,6 +900,8 @@ sealed class ActionData : Comparable { val nodeAction: NodeInteractionType, val packageName: String, val text: String?, + val tooltip: String?, + val hint: String?, val contentDescription: String?, val className: String?, val viewResourceId: String?, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index 4133b18d4d..b56a2856ee 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.actions +import androidx.core.net.toUri import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import io.github.sds100.keymapper.data.db.typeconverter.ConstantTypeConverters @@ -239,11 +240,24 @@ object ActionDataEntityMapper { ActionId.PHONE_CALL -> ActionData.PhoneCall(number = entity.data) ActionId.SOUND -> { - val soundFileDescription = - entity.extras.getData(ActionEntity.EXTRA_SOUND_FILE_DESCRIPTION) - .valueOrNull() ?: return null + val isRingtoneUri = try { + entity.data.toUri().scheme != null + } catch (e: Exception) { + false + } + + if (isRingtoneUri) { + return ActionData.Sound.Ringtone(entity.data) + } else { + val soundFileDescription = + entity.extras.getData(ActionEntity.EXTRA_SOUND_FILE_DESCRIPTION) + .valueOrNull() ?: return null - ActionData.Sound(soundUid = entity.data, soundDescription = soundFileDescription) + ActionData.Sound.SoundFile( + soundUid = entity.data, + soundDescription = soundFileDescription, + ) + } } ActionId.VOLUME_INCREASE_STREAM, @@ -356,6 +370,9 @@ object ActionDataEntityMapper { ActionId.PREVIOUS_TRACK_PACKAGE, ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, + ActionId.STOP_MEDIA_PACKAGE, + ActionId.STEP_FORWARD_PACKAGE, + ActionId.STEP_BACKWARD_PACKAGE, -> { val packageName = entity.extras.getData(ActionEntity.EXTRA_PACKAGE_NAME).valueOrNull() @@ -383,6 +400,15 @@ object ActionDataEntityMapper { ActionId.REWIND_PACKAGE -> ActionData.ControlMediaForApp.Rewind(packageName) + ActionId.STOP_MEDIA_PACKAGE -> + ActionData.ControlMediaForApp.Stop(packageName) + + ActionId.STEP_FORWARD_PACKAGE -> + ActionData.ControlMediaForApp.StepForward(packageName) + + ActionId.STEP_BACKWARD_PACKAGE -> + ActionData.ControlMediaForApp.StepBackward(packageName) + else -> throw Exception("don't know how to create system action for $actionId") } } @@ -461,6 +487,9 @@ object ActionDataEntityMapper { ActionId.PREVIOUS_TRACK -> ActionData.ControlMedia.PreviousTrack ActionId.FAST_FORWARD -> ActionData.ControlMedia.FastForward ActionId.REWIND -> ActionData.ControlMedia.Rewind + ActionId.STOP_MEDIA -> ActionData.ControlMedia.Stop + ActionId.STEP_FORWARD -> ActionData.ControlMedia.StepForward + ActionId.STEP_BACKWARD -> ActionData.ControlMedia.StepBackward ActionId.GO_BACK -> ActionData.GoBack ActionId.GO_HOME -> ActionData.GoHome @@ -542,6 +571,12 @@ object ActionDataEntityMapper { val text = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TEXT).valueOrNull() + val tooltip = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TOOLTIP).valueOrNull() + + val hint = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_HINT).valueOrNull() + val className = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_CLASS_NAME).valueOrNull() @@ -567,6 +602,8 @@ object ActionDataEntityMapper { packageName = packageName, text = text, contentDescription = contentDescription, + tooltip = tooltip, + hint = hint, className = className, viewResourceId = viewResourceId, uniqueId = uniqueId, @@ -636,8 +673,17 @@ object ActionDataEntityMapper { is ActionData.PinchScreen -> "${data.x},${data.y},${data.distance},${data.pinchType},${data.fingerCount},${data.duration}" is ActionData.Text -> data.text is ActionData.Url -> data.url - is ActionData.Sound -> data.soundUid + is ActionData.Sound -> when (data) { + is ActionData.Sound.Ringtone -> data.uri + is ActionData.Sound.SoundFile -> data.soundUid + } + is ActionData.InteractUiElement -> data.description + is ActionData.ControlMediaForApp.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!! + is ActionData.ControlMediaForApp.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!! + is ActionData.ControlMedia.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!! + is ActionData.ControlMedia.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!! + is ActionData.GoBack -> SYSTEM_ACTION_ID_MAP[data.id]!! else -> SYSTEM_ACTION_ID_MAP[data.id]!! } @@ -794,7 +840,7 @@ object ActionDataEntityMapper { is ActionData.Text -> emptyList() is ActionData.Url -> emptyList() - is ActionData.Sound -> listOf( + is ActionData.Sound.SoundFile -> listOf( EntityExtra(ActionEntity.EXTRA_SOUND_FILE_DESCRIPTION, data.soundDescription), ) @@ -835,6 +881,10 @@ object ActionDataEntityMapper { data.text?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_TEXT, it)) } + data.tooltip?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_TOOLTIP, it)) } + + data.hint?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_HINT, it)) } + data.className?.let { add( EntityExtra( @@ -982,6 +1032,12 @@ object ActionDataEntityMapper { ActionId.FAST_FORWARD_PACKAGE to "fast_forward_package", ActionId.REWIND to "rewind", ActionId.REWIND_PACKAGE to "rewind_package", + ActionId.STOP_MEDIA to "stop_media", + ActionId.STOP_MEDIA_PACKAGE to "stop_media_package", + ActionId.STEP_FORWARD to "step_forward", + ActionId.STEP_FORWARD_PACKAGE to "step_forward_package", + ActionId.STEP_BACKWARD to "step_backward", + ActionId.STEP_BACKWARD_PACKAGE to "step_backward_package", ActionId.GO_BACK to "go_back", ActionId.GO_HOME to "go_home", diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionErrorSnapshot.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionErrorSnapshot.kt index 9ba101fa40..11c4129b8c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionErrorSnapshot.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionErrorSnapshot.kt @@ -10,6 +10,7 @@ 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.system.permissions.SystemFeatureAdapter +import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.onFailure import io.github.sds100.keymapper.util.onSuccess @@ -22,6 +23,7 @@ class LazyActionErrorSnapshot( cameraAdapter: CameraAdapter, private val soundsManager: SoundsManager, shizukuAdapter: ShizukuAdapter, + private val ringtoneAdapter: RingtoneAdapter, ) : ActionErrorSnapshot, IsActionSupportedUseCase by IsActionSupportedUseCaseImpl( systemFeatureAdapter, @@ -99,12 +101,18 @@ class LazyActionErrorSnapshot( return Error.PermissionDenied(Permission.ROOT) } - is ActionData.Sound -> { + is ActionData.Sound.SoundFile -> { soundsManager.getSound(action.soundUid).onFailure { error -> return error } } + is ActionData.Sound.Ringtone -> { + if (!ringtoneAdapter.exists(action.uri)) { + return Error.CantFindSoundFile + } + } + is ActionData.VoiceAssistant -> { if (!isVoiceAssistantInstalled) { return Error.NoVoiceAssistant diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt index 011c7f825a..80840e9949 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt @@ -8,14 +8,14 @@ enum class ActionId { APP_SHORTCUT, KEY_CODE, KEY_EVENT, + TEXT, TAP_SCREEN, SWIPE_SCREEN, PINCH_SCREEN, - TEXT, URL, + HTTP_REQUEST, INTENT, PHONE_CALL, - SOUND, INTERACT_UI_ELEMENT, TOGGLE_WIFI, @@ -65,6 +65,7 @@ enum class ActionId { TOGGLE_QUICK_SETTINGS, COLLAPSE_STATUS_BAR, + SOUND, PAUSE_MEDIA, PAUSE_MEDIA_PACKAGE, PLAY_MEDIA, @@ -79,6 +80,12 @@ enum class ActionId { FAST_FORWARD_PACKAGE, REWIND, REWIND_PACKAGE, + STOP_MEDIA, + STOP_MEDIA_PACKAGE, + STEP_FORWARD, + STEP_FORWARD_PACKAGE, + STEP_BACKWARD, + STEP_BACKWARD_PACKAGE, GO_BACK, GO_HOME, @@ -96,15 +103,15 @@ enum class ActionId { DISABLE_NFC, TOGGLE_NFC, + TEXT_CUT, + TEXT_COPY, + TEXT_PASTE, MOVE_CURSOR_TO_END, + SELECT_WORD_AT_CURSOR, TOGGLE_KEYBOARD, SHOW_KEYBOARD, HIDE_KEYBOARD, SHOW_KEYBOARD_PICKER, - TEXT_CUT, - TEXT_COPY, - TEXT_PASTE, - SELECT_WORD_AT_CURSOR, SWITCH_KEYBOARD, @@ -130,6 +137,4 @@ enum class ActionId { ANSWER_PHONE_CALL, END_PHONE_CALL, DEVICE_CONTROLS, - - HTTP_REQUEST, } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt index 0ce11b41af..92276cfeb8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt @@ -44,10 +44,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.compose.draggable.DragDropState import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.LinkType import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.DragDropState @Composable fun ActionListItem( diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt index ccd1fac039..723084996b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt @@ -6,7 +6,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Android import io.github.sds100.keymapper.R import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType -import io.github.sds100.keymapper.mappings.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMap import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.devices.InputDeviceUtils import io.github.sds100.keymapper.system.display.OrientationUtils @@ -222,6 +222,9 @@ class ActionUiHelper( is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package_formatted is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package_formatted is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package_formatted + is ActionData.ControlMediaForApp.Stop -> R.string.action_stop_media_package_formatted + is ActionData.ControlMediaForApp.StepForward -> R.string.action_step_forward_media_package_formatted + is ActionData.ControlMediaForApp.StepBackward -> R.string.action_step_backward_media_package_formatted } getString(resId, appName) @@ -235,6 +238,9 @@ class ActionUiHelper( is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package + is ActionData.ControlMediaForApp.Stop -> R.string.action_stop_media_package + is ActionData.ControlMediaForApp.StepForward -> R.string.action_step_forward_media_package + is ActionData.ControlMediaForApp.StepBackward -> R.string.action_step_backward_media_package } getString(resId) @@ -428,7 +434,17 @@ class ActionUiHelper( is ActionData.Text -> getString(R.string.description_text_block, action.text) is ActionData.Url -> getString(R.string.description_url, action.url) - is ActionData.Sound -> getString(R.string.description_sound, action.soundDescription) + is ActionData.Sound.SoundFile -> getString( + R.string.description_sound, + action.soundDescription, + ) + + is ActionData.Sound.Ringtone -> { + getRingtoneLabel(action.uri).handle( + onSuccess = { getString(R.string.description_sound, it) }, + onError = { getString(R.string.description_sound_unknown) }, + ) + } ActionData.AirplaneMode.Disable -> getString(R.string.action_disable_airplane_mode) ActionData.AirplaneMode.Enable -> getString(R.string.action_enable_airplane_mode) @@ -453,6 +469,9 @@ class ActionUiHelper( ActionData.ControlMedia.PlayPause -> getString(R.string.action_play_pause_media) ActionData.ControlMedia.PreviousTrack -> getString(R.string.action_previous_track) ActionData.ControlMedia.Rewind -> getString(R.string.action_rewind) + ActionData.ControlMedia.Stop -> getString(R.string.action_stop_media) + ActionData.ControlMedia.StepForward -> getString(R.string.action_step_forward_media) + ActionData.ControlMedia.StepBackward -> getString(R.string.action_step_backward_media) ActionData.CopyText -> getString(R.string.action_text_copy) ActionData.CutText -> getString(R.string.action_text_cut) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt index a2a0d82c73..f7833f70f0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt @@ -30,6 +30,7 @@ import androidx.compose.material.icons.outlined.FastForward import androidx.compose.material.icons.outlined.FastRewind import androidx.compose.material.icons.outlined.FlashlightOff import androidx.compose.material.icons.outlined.FlashlightOn +import androidx.compose.material.icons.outlined.Forward30 import androidx.compose.material.icons.outlined.Fullscreen import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Http @@ -45,6 +46,7 @@ import androidx.compose.material.icons.outlined.PhonelinkRing import androidx.compose.material.icons.outlined.Pinch import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.PowerSettingsNew +import androidx.compose.material.icons.outlined.Replay30 import androidx.compose.material.icons.outlined.ScreenLockRotation import androidx.compose.material.icons.outlined.ScreenRotation import androidx.compose.material.icons.outlined.Settings @@ -55,6 +57,7 @@ import androidx.compose.material.icons.outlined.SkipPrevious import androidx.compose.material.icons.outlined.Splitscreen import androidx.compose.material.icons.outlined.StayCurrentLandscape import androidx.compose.material.icons.outlined.StayCurrentPortrait +import androidx.compose.material.icons.outlined.StopCircle import androidx.compose.material.icons.outlined.Swipe import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.material.icons.outlined.ViewArray @@ -95,17 +98,17 @@ object ActionUtils { ActionCategory.KEYBOARD -> R.string.action_cat_keyboard ActionCategory.APPS -> R.string.action_cat_apps ActionCategory.INPUT -> R.string.action_cat_input - ActionCategory.CAMERA_SOUND -> R.string.action_cat_camera_sound + ActionCategory.FLASHLIGHT -> R.string.action_cat_flashlight ActionCategory.CONNECTIVITY -> R.string.action_cat_connectivity ActionCategory.CONTENT -> R.string.action_cat_content ActionCategory.INTERFACE -> R.string.action_cat_interface ActionCategory.TELEPHONY -> R.string.action_cat_telephony ActionCategory.DISPLAY -> R.string.action_cat_display ActionCategory.NOTIFICATIONS -> R.string.action_cat_notifications + ActionCategory.SPECIAL -> R.string.action_cat_special } fun getCategory(id: ActionId): ActionCategory = when (id) { - ActionId.CONSUME_KEY_EVENT -> ActionCategory.INPUT ActionId.KEY_CODE -> ActionCategory.INPUT ActionId.KEY_EVENT -> ActionCategory.INPUT ActionId.TAP_SCREEN -> ActionCategory.INPUT @@ -168,6 +171,7 @@ object ActionUtils { ActionId.TOGGLE_QUICK_SETTINGS -> ActionCategory.NAVIGATION ActionId.COLLAPSE_STATUS_BAR -> ActionCategory.NAVIGATION + ActionId.SOUND -> ActionCategory.MEDIA ActionId.PAUSE_MEDIA -> ActionCategory.MEDIA ActionId.PAUSE_MEDIA_PACKAGE -> ActionCategory.MEDIA ActionId.PLAY_MEDIA -> ActionCategory.MEDIA @@ -182,6 +186,12 @@ object ActionUtils { ActionId.FAST_FORWARD_PACKAGE -> ActionCategory.MEDIA ActionId.REWIND -> ActionCategory.MEDIA ActionId.REWIND_PACKAGE -> ActionCategory.MEDIA + ActionId.STOP_MEDIA -> ActionCategory.MEDIA + ActionId.STOP_MEDIA_PACKAGE -> ActionCategory.MEDIA + ActionId.STEP_FORWARD -> ActionCategory.MEDIA + ActionId.STEP_FORWARD_PACKAGE -> ActionCategory.MEDIA + ActionId.STEP_BACKWARD -> ActionCategory.MEDIA + ActionId.STEP_BACKWARD_PACKAGE -> ActionCategory.MEDIA ActionId.GO_BACK -> ActionCategory.NAVIGATION ActionId.GO_HOME -> ActionCategory.NAVIGATION @@ -190,11 +200,10 @@ object ActionUtils { ActionId.GO_LAST_APP -> ActionCategory.NAVIGATION ActionId.OPEN_MENU -> ActionCategory.NAVIGATION - ActionId.TOGGLE_FLASHLIGHT -> ActionCategory.CAMERA_SOUND - ActionId.ENABLE_FLASHLIGHT -> ActionCategory.CAMERA_SOUND - ActionId.DISABLE_FLASHLIGHT -> ActionCategory.CAMERA_SOUND - ActionId.CHANGE_FLASHLIGHT_STRENGTH -> ActionCategory.CAMERA_SOUND - ActionId.SOUND -> ActionCategory.CAMERA_SOUND + ActionId.TOGGLE_FLASHLIGHT -> ActionCategory.FLASHLIGHT + ActionId.ENABLE_FLASHLIGHT -> ActionCategory.FLASHLIGHT + ActionId.DISABLE_FLASHLIGHT -> ActionCategory.FLASHLIGHT + ActionId.CHANGE_FLASHLIGHT_STRENGTH -> ActionCategory.FLASHLIGHT ActionId.ENABLE_NFC -> ActionCategory.CONNECTIVITY ActionId.DISABLE_NFC -> ActionCategory.CONNECTIVITY @@ -233,6 +242,8 @@ object ActionUtils { ActionId.DEVICE_CONTROLS -> ActionCategory.APPS ActionId.INTERACT_UI_ELEMENT -> ActionCategory.APPS + + ActionId.CONSUME_KEY_EVENT -> ActionCategory.SPECIAL } @StringRes @@ -291,6 +302,12 @@ object ActionUtils { ActionId.FAST_FORWARD_PACKAGE -> R.string.action_fast_forward_package ActionId.REWIND -> R.string.action_rewind ActionId.REWIND_PACKAGE -> R.string.action_rewind_package + ActionId.STOP_MEDIA -> R.string.action_stop_media + ActionId.STOP_MEDIA_PACKAGE -> R.string.action_stop_media_package + ActionId.STEP_FORWARD -> R.string.action_step_forward_media + ActionId.STEP_FORWARD_PACKAGE -> R.string.action_step_forward_media_package + ActionId.STEP_BACKWARD -> R.string.action_step_backward_media + ActionId.STEP_BACKWARD_PACKAGE -> R.string.action_step_backward_media_package ActionId.GO_BACK -> R.string.action_go_back ActionId.GO_HOME -> R.string.action_go_home ActionId.OPEN_RECENTS -> R.string.action_open_recents @@ -404,6 +421,12 @@ object ActionUtils { ActionId.FAST_FORWARD_PACKAGE -> R.drawable.ic_outline_fast_forward_24 ActionId.REWIND -> R.drawable.ic_outline_fast_rewind_24 ActionId.REWIND_PACKAGE -> R.drawable.ic_outline_fast_rewind_24 + ActionId.STOP_MEDIA -> R.drawable.ic_outline_pause_24 + ActionId.STOP_MEDIA_PACKAGE -> R.drawable.ic_outline_pause_24 + ActionId.STEP_FORWARD -> null + ActionId.STEP_FORWARD_PACKAGE -> null + ActionId.STEP_BACKWARD -> null + ActionId.STEP_BACKWARD_PACKAGE -> null ActionId.GO_BACK -> R.drawable.ic_baseline_arrow_back_24 ActionId.GO_HOME -> R.drawable.ic_outline_home_24 ActionId.OPEN_RECENTS -> null @@ -721,6 +744,12 @@ object ActionUtils { ActionId.FAST_FORWARD_PACKAGE -> Icons.Outlined.FastForward ActionId.REWIND -> Icons.Outlined.FastRewind ActionId.REWIND_PACKAGE -> Icons.Outlined.FastRewind + ActionId.STOP_MEDIA -> Icons.Outlined.StopCircle + ActionId.STOP_MEDIA_PACKAGE -> Icons.Outlined.StopCircle + ActionId.STEP_FORWARD -> Icons.Outlined.Forward30 + ActionId.STEP_FORWARD_PACKAGE -> Icons.Outlined.Forward30 + ActionId.STEP_BACKWARD -> Icons.Outlined.Replay30 + ActionId.STEP_BACKWARD_PACKAGE -> Icons.Outlined.Replay30 ActionId.GO_BACK -> Icons.AutoMirrored.Outlined.ArrowBack ActionId.GO_HOME -> Icons.Outlined.Home ActionId.OPEN_RECENTS -> Icons.Outlined.ViewArray diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt index 003235c51e..e53596658e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt @@ -39,14 +39,14 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.compose.draggable.DraggableItem -import io.github.sds100.keymapper.compose.draggable.rememberDragDropState -import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel -import io.github.sds100.keymapper.mappings.keymaps.ShortcutRow +import io.github.sds100.keymapper.keymaps.ShortcutModel +import io.github.sds100.keymapper.keymaps.ShortcutRow import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.ui.LinkType import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.DraggableItem +import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState import kotlinx.coroutines.flow.update @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionFragment.kt index 7abaeb4976..8bc9c8f483 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionFragment.kt @@ -4,7 +4,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf @@ -25,7 +32,6 @@ import io.github.sds100.keymapper.util.ui.showPopups import io.github.sds100.keymapper.util.viewLifecycleScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json class ChooseActionFragment : Fragment() { @@ -73,7 +79,12 @@ class ChooseActionFragment : Fragment() { setContent { KeyMapperTheme { ChooseActionScreen( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ), viewModel = viewModel, onNavigateBack = findNavController()::navigateUp, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionViewModel.kt index ad7f075949..10ea36c237 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionViewModel.kt @@ -46,17 +46,18 @@ class ChooseActionViewModel( private val CATEGORY_ORDER = arrayOf( ActionCategory.INPUT, ActionCategory.APPS, + ActionCategory.FLASHLIGHT, + ActionCategory.CONTENT, ActionCategory.NAVIGATION, ActionCategory.VOLUME, ActionCategory.DISPLAY, ActionCategory.MEDIA, ActionCategory.INTERFACE, - ActionCategory.CONTENT, ActionCategory.KEYBOARD, ActionCategory.CONNECTIVITY, ActionCategory.TELEPHONY, - ActionCategory.CAMERA_SOUND, ActionCategory.NOTIFICATIONS, + ActionCategory.SPECIAL, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt index 96d6da8bb0..dfe362ff0b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt @@ -1,9 +1,9 @@ package io.github.sds100.keymapper.actions import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.mappings.keymaps.KeyMap -import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel +import io.github.sds100.keymapper.keymaps.ConfigKeyMapUseCase +import io.github.sds100.keymapper.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.ShortcutModel import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.util.Error @@ -177,7 +177,6 @@ class ConfigActionsViewModel( override fun onEditClick() { val actionUid = actionOptionsUid.value ?: return coroutineScope.launch { - actionOptionsUid.update { null } val keyMap = config.keyMap.first().dataOrNull() ?: return@launch val oldAction = keyMap.actionList.find { it.uid == actionUid } ?: return@launch diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt index 649ef42bd7..e034b3e9f8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt @@ -181,6 +181,9 @@ class CreateActionDelegate( ActionId.PREVIOUS_TRACK_PACKAGE, ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, + ActionId.STOP_MEDIA_PACKAGE, + ActionId.STEP_FORWARD_PACKAGE, + ActionId.STEP_BACKWARD_PACKAGE, -> { val packageName = navigate( @@ -211,6 +214,15 @@ class CreateActionDelegate( ActionId.REWIND_PACKAGE -> ActionData.ControlMediaForApp.Rewind(packageName) + ActionId.STOP_MEDIA_PACKAGE -> + ActionData.ControlMediaForApp.Stop(packageName) + + ActionId.STEP_FORWARD_PACKAGE -> + ActionData.ControlMediaForApp.StepForward(packageName) + + ActionId.STEP_BACKWARD_PACKAGE -> + ActionData.ControlMediaForApp.StepBackward(packageName) + else -> throw Exception("don't know how to create action for $actionId") } @@ -676,14 +688,9 @@ class CreateActionDelegate( } ActionId.SOUND -> { - val result = navigate( + return navigate( "choose_sound_file", NavDestination.ChooseSound, - ) ?: return null - - return ActionData.Sound( - soundUid = result.soundUid, - soundDescription = result.description, ) } @@ -729,6 +736,9 @@ class CreateActionDelegate( ActionId.PREVIOUS_TRACK -> return ActionData.ControlMedia.PreviousTrack ActionId.FAST_FORWARD -> return ActionData.ControlMedia.FastForward ActionId.REWIND -> return ActionData.ControlMedia.Rewind + ActionId.STOP_MEDIA -> return ActionData.ControlMedia.Stop + ActionId.STEP_FORWARD -> return ActionData.ControlMedia.StepForward + ActionId.STEP_BACKWARD -> return ActionData.ControlMedia.StepBackward ActionId.GO_BACK -> return ActionData.GoBack ActionId.GO_HOME -> return ActionData.GoHome diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/DisplayActionUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/DisplayActionUseCase.kt index 65b59b2bde..4166825086 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/DisplayActionUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/DisplayActionUseCase.kt @@ -10,6 +10,7 @@ interface DisplayActionUseCase : GetActionErrorUseCase { fun getAppName(packageName: String): Result fun getAppIcon(packageName: String): Result fun getInputMethodLabel(imeId: String): Result + fun getRingtoneLabel(uri: String): Result suspend fun fixError(error: Error) fun neverShowDndTriggerError() fun startAccessibilityService(): Boolean diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/GetActionErrorUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/GetActionErrorUseCase.kt index e8e1b6c9d9..ce8a18b9e1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/GetActionErrorUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/GetActionErrorUseCase.kt @@ -7,6 +7,7 @@ import io.github.sds100.keymapper.system.camera.CameraAdapter import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter +import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest @@ -22,6 +23,7 @@ class GetActionErrorUseCaseImpl( private val cameraAdapter: CameraAdapter, private val soundsManager: SoundsManager, private val shizukuAdapter: ShizukuAdapter, + private val ringtoneAdapter: RingtoneAdapter, ) : GetActionErrorUseCase { private val invalidateActionErrors = merge( @@ -51,6 +53,7 @@ class GetActionErrorUseCaseImpl( cameraAdapter, soundsManager, shizukuAdapter, + ringtoneAdapter, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 80c6ea900d..11d66a7ae0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.actions import android.accessibilityservice.AccessibilityService import android.os.Build +import android.view.InputDevice import android.view.KeyEvent import android.view.accessibility.AccessibilityNodeInfo import io.github.sds100.keymapper.R @@ -39,6 +40,7 @@ import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.phone.PhoneAdapter import io.github.sds100.keymapper.system.popup.PopupMessageAdapter +import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.shell.ShellAdapter import io.github.sds100.keymapper.system.url.OpenUrlAdapter @@ -107,6 +109,7 @@ class PerformActionsUseCaseImpl( private val soundsManager: SoundsManager, private val permissionAdapter: PermissionAdapter, private val notificationReceiverAdapter: ServiceAdapter, + private val ringtoneAdapter: RingtoneAdapter, ) : PerformActionsUseCase { private val openMenuHelper by lazy { @@ -152,11 +155,19 @@ class PerformActionsUseCaseImpl( is ActionData.InputKeyEvent -> { val deviceId: Int = getDeviceIdForKeyEventAction(action) + // See issue #1683. Some apps ignore key events which do not have a source. + val source = when { + InputEventUtils.isDpadKeyCode(action.keyCode) -> InputDevice.SOURCE_DPAD + InputEventUtils.isGamepadButton(action.keyCode) -> InputDevice.SOURCE_GAMEPAD + else -> InputDevice.SOURCE_KEYBOARD + } + val model = InputKeyModel( keyCode = action.keyCode, inputType = inputEventType, metaState = keyMetaState.withFlag(action.metaState), deviceId = deviceId, + source = source, ) result = when { @@ -223,6 +234,18 @@ class PerformActionsUseCaseImpl( result = mediaAdapter.rewind(action.packageName) } + is ActionData.ControlMediaForApp.Stop -> { + result = mediaAdapter.stop(action.packageName) + } + + is ActionData.ControlMediaForApp.StepForward -> { + result = mediaAdapter.stepForward(action.packageName) + } + + is ActionData.ControlMediaForApp.StepBackward -> { + result = mediaAdapter.stepBackward(action.packageName) + } + is ActionData.Rotation.CycleRotations -> { result = displayAdapter.disableAutoRotate().then { val currentOrientation = displayAdapter.cachedOrientation @@ -338,9 +361,16 @@ class PerformActionsUseCaseImpl( result = openUrlAdapter.openUrl(action.url) } - is ActionData.Sound -> { + is ActionData.Sound.SoundFile -> { + ringtoneAdapter.stopPlaying() result = soundsManager.getSound(action.soundUid).then { file -> - mediaAdapter.playSoundFile(file.uri, VolumeStream.ACCESSIBILITY) + mediaAdapter.playFile(file.uri, VolumeStream.ACCESSIBILITY) + } + } + + is ActionData.Sound.Ringtone -> { + result = mediaAdapter.stopFileMedia().then { + ringtoneAdapter.play(action.uri) } } @@ -547,6 +577,18 @@ class PerformActionsUseCaseImpl( result = mediaAdapter.rewind() } + is ActionData.ControlMedia.Stop -> { + result = mediaAdapter.stop() + } + + is ActionData.ControlMedia.StepForward -> { + result = mediaAdapter.stepForward() + } + + is ActionData.ControlMedia.StepBackward -> { + result = mediaAdapter.stepBackward() + } + is ActionData.GoBack -> { result = accessibilityService.doGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) @@ -811,7 +853,7 @@ class PerformActionsUseCaseImpl( matchAccessibilityNode(node, action) }, performAction = { AccessibilityNodeAction(action = action.nodeAction.accessibilityActionId) }, - ) + ).otherwise { Error.UiElementNotFound } } } } @@ -911,32 +953,36 @@ class PerformActionsUseCaseImpl( node: AccessibilityNodeModel, action: ActionData.InteractUiElement, ): Boolean { + if (!node.actions.contains(action.nodeAction.accessibilityActionId)) { + return false + } + if (compareIfNonNull(node.uniqueId, action.uniqueId)) { return true } - val viewResourceIdMatches = node.viewResourceId == action.viewResourceId - val classNameMatches = node.className == action.className + if (action.contentDescription == null && action.text == null) { + if (compareIfNonNull(node.viewResourceId, action.viewResourceId)) { + return true + } - if (compareIfNonNull( - node.contentDescription, - action.contentDescription, - ) && - viewResourceIdMatches && - classNameMatches - ) { - return true - } + if (compareIfNonNull(node.className, action.className)) { + return true + } + } else { + if (compareIfNonNull(node.contentDescription, action.contentDescription) || + compareIfNonNull(node.text, action.text) + ) { + if (action.viewResourceId != null) { + return node.viewResourceId == action.viewResourceId + } - if (compareIfNonNull(node.text, action.text) && - viewResourceIdMatches && - classNameMatches - ) { - return true - } + if (action.className != null) { + return node.className == action.className + } - if (viewResourceIdMatches) { - return true + return true + } } return false diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt index 6f90c1ceaa..1f6dfcf81d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt @@ -51,6 +51,14 @@ class ConfigKeyEventActionFragment : Fragment() { val binding: FragmentConfigKeyEventBinding get() = _binding!! + private val deviceArrayAdapter: ArrayAdapter by lazy { + ArrayAdapter( + requireContext(), + R.layout.dropdown_menu_popup_item, + mutableListOf(), + ) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -98,47 +106,9 @@ class ConfigKeyEventActionFragment : Fragment() { findNavController().navigateUp() } - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.returnResult.collectLatest { - setFragmentResult( - requestKey, - Bundle().apply { putJsonSerializable(EXTRA_RESULT, it) }, - ) - - findNavController().navigateUp() - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.uiState.collectLatest { state -> - binding.epoxyRecyclerViewModifiers.withModels { - state.modifierListItems.forEach { listItem -> - configuredCheckBox(listItem) { isChecked -> - viewModel.setModifierKeyChecked(listItem.id.toInt(), isChecked) - } - } - } - - ArrayAdapter( - requireContext(), - R.layout.dropdown_menu_popup_item, - mutableListOf(), - ).apply { - clear() - add(str(R.string.from_no_device)) - - state.deviceListItems.forEach { - add(it.name) - } - - binding.dropdownDeviceId.setAdapter(this) - } - - binding.textInputLayoutKeyCode.error = state.keyCodeErrorMessage - } - } - binding.dropdownDeviceId.apply { + setAdapter(deviceArrayAdapter) + // set the default value setText(str(R.string.from_no_device), false) @@ -173,12 +143,44 @@ class ConfigKeyEventActionFragment : Fragment() { } } } - } - override fun onResume() { - super.onResume() + viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.returnResult.collectLatest { + setFragmentResult( + requestKey, + Bundle().apply { putJsonSerializable(EXTRA_RESULT, it) }, + ) + + findNavController().navigateUp() + } + } - viewModel.rebuildUiState() + viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.uiState.collect { state -> + binding.epoxyRecyclerViewModifiers.withModels { + state.modifierListItems.forEach { listItem -> + configuredCheckBox(listItem) { isChecked -> + viewModel.setModifierKeyChecked(listItem.id.toInt(), isChecked) + } + } + } + + deviceArrayAdapter.apply { + clear() + add(str(R.string.from_no_device)) + for (device in state.deviceListItems) { + add(device.name) + } + notifyDataSetChanged() + } + + // Filtering must be false so that the dropdown items aren't cleared + // when setting text. + binding.dropdownDeviceId.setText(state.chosenDeviceName, false) + + binding.textInputLayoutKeyCode.error = state.keyCodeErrorMessage + } + } } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt index e6734d0921..30d1236f18 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt @@ -27,13 +27,13 @@ import io.github.sds100.keymapper.util.ui.navigate import io.github.sds100.keymapper.util.valueOrNull import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import splitties.bitflags.hasFlag import splitties.bitflags.minusFlag import splitties.bitflags.withFlag @@ -51,35 +51,25 @@ class ConfigKeyEventActionViewModel( private val keyEventState = MutableStateFlow(KeyEventState()) - private val _uiState = MutableStateFlow( + val uiState: StateFlow = combine( + keyEventState, + useCase.inputDevices, + useCase.showDeviceDescriptors, + ) { state, inputDevices, showDeviceDescriptors -> + buildUiState(state, inputDevices, showDeviceDescriptors) + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, buildUiState( keyEventState.value, inputDeviceList = emptyList(), showDeviceDescriptors = false, ), ) - val uiState = _uiState.asStateFlow() private val _returnResult = MutableSharedFlow() val returnResult = _returnResult.asSharedFlow() - private val rebuildUiState = MutableSharedFlow() - - init { - viewModelScope.launch { - - combine( - keyEventState, - useCase.inputDevices, - useCase.showDeviceDescriptors, - ) { state, inputDevices, showDeviceDescriptors -> - buildUiState(state, inputDevices, showDeviceDescriptors) - }.collectLatest { - _uiState.value = it - } - } - } - fun setModifierKeyChecked(modifier: Int, isChecked: Boolean) { val oldMetaState = keyEventState.value.metaState @@ -143,17 +133,15 @@ class ConfigKeyEventActionViewModel( } fun chooseDevice(index: Int) { - viewModelScope.launch { - val chosenDevice = uiState.value.deviceListItems.getOrNull(index) - - if (chosenDevice == null) { - return@launch - } + val chosenDevice = uiState.value.deviceListItems.getOrNull(index) - keyEventState.value = keyEventState.value.copy( - chosenDevice = chosenDevice, - ) + if (chosenDevice == null) { + return } + + keyEventState.value = keyEventState.value.copy( + chosenDevice = chosenDevice, + ) } fun onDoneClick() { @@ -178,14 +166,6 @@ class ConfigKeyEventActionViewModel( } } - fun refreshDevices() { - rebuildUiState() - } - - fun rebuildUiState() { - runBlocking { rebuildUiState.emit(Unit) } - } - private fun buildUiState( state: KeyEventState, inputDeviceList: List, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileFragment.kt index 4eb05e9058..9584e52bd0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileFragment.kt @@ -1,11 +1,16 @@ package io.github.sds100.keymapper.actions.sound +import android.app.Activity +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.IntentCompat import androidx.core.os.bundleOf import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -23,7 +28,8 @@ import io.github.sds100.keymapper.util.Inject import io.github.sds100.keymapper.util.launchRepeatOnLifecycle import io.github.sds100.keymapper.util.ui.showPopups import kotlinx.coroutines.flow.collectLatest -import kotlinx.serialization.encodeToString +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.update import kotlinx.serialization.json.Json /** @@ -49,6 +55,19 @@ class ChooseSoundFileFragment : Fragment() { viewModel.onChooseNewSoundFile(it.toString()) } + private val chooseRingtoneLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.data != null && result.resultCode == Activity.RESULT_OK) { + val uri = IntentCompat.getParcelableExtra( + result.data!!, + RingtoneManager.EXTRA_RINGTONE_PICKED_URI, + Uri::class.java, + ) ?: return@registerForActivityResult + + viewModel.onChooseRingtone(uri.toString()) + } + } + /** * Scoped to the lifecycle of the fragment's view (between onCreateView and onDestroyView) */ @@ -87,6 +106,13 @@ class ChooseSoundFileFragment : Fragment() { } } + viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.chooseSystemRingtone.collectLatest { + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) + chooseRingtoneLauncher.launch(intent) + } + } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { findNavController().navigateUp() } @@ -96,12 +122,13 @@ class ChooseSoundFileFragment : Fragment() { } viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.returnResult.collectLatest { result -> + viewModel.returnResult.filterNotNull().collect { result -> setFragmentResult( requestKey, bundleOf(EXTRA_RESULT to Json.encodeToString(result)), ) findNavController().navigateUp() + viewModel.returnResult.update { null } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileResult.kt b/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileResult.kt deleted file mode 100644 index a019055164..0000000000 --- a/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileResult.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.sds100.keymapper.actions.sound - -import kotlinx.serialization.Serializable - -/** - * Created by sds100 on 22/06/2021. - */ -@Serializable -data class ChooseSoundFileResult(val soundUid: String, val description: String) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileViewModel.kt index 79f478a536..4967227dd6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/sound/ChooseSoundFileViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.util.getFullMessage import io.github.sds100.keymapper.util.onFailure import io.github.sds100.keymapper.util.onSuccess @@ -15,11 +16,13 @@ import io.github.sds100.keymapper.util.ui.ResourceProvider import io.github.sds100.keymapper.util.ui.showPopup import io.github.sds100.keymapper.util.valueOrNull import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** @@ -35,6 +38,9 @@ class ChooseSoundFileViewModel( private val _chooseSoundFile = MutableSharedFlow() val chooseSoundFile = _chooseSoundFile.asSharedFlow() + private val _chooseSystemRingtone = MutableSharedFlow() + val chooseSystemRingtone = _chooseSystemRingtone.asSharedFlow() + val soundFileListItems: StateFlow> = useCase.soundFiles.map { sounds -> sounds.map { @@ -42,8 +48,8 @@ class ChooseSoundFileViewModel( } }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - private val _returnResult = MutableSharedFlow() - val returnResult = _returnResult.asSharedFlow() + val returnResult: MutableStateFlow = + MutableStateFlow(null) fun onChooseSoundFileButtonClick() { viewModelScope.launch { @@ -51,6 +57,12 @@ class ChooseSoundFileViewModel( } } + fun onChooseSystemRingtoneButtonClick() { + viewModelScope.launch { + _chooseSystemRingtone.emit(Unit) + } + } + fun onFileListItemClick(id: String) { viewModelScope.launch { val soundFileInfo = useCase.soundFiles.value.find { it.uid == id } ?: return@launch @@ -63,12 +75,12 @@ class ChooseSoundFileViewModel( val soundDescription = showPopup("file_description", dialog) ?: return@launch - _returnResult.emit( - ChooseSoundFileResult( + returnResult.update { + ActionData.Sound.SoundFile( soundUid = soundFileInfo.uid, - description = soundDescription, - ), - ) + soundDescription = soundDescription, + ) + } } } @@ -88,7 +100,12 @@ class ChooseSoundFileViewModel( useCase.saveSound(uri) .onSuccess { soundFileUid -> - _returnResult.emit(ChooseSoundFileResult(soundFileUid, soundDescription)) + returnResult.update { + ActionData.Sound.SoundFile( + soundFileUid, + soundDescription, + ) + } }.onFailure { error -> val toast = PopupUi.Toast(error.getFullMessage(this@ChooseSoundFileViewModel)) showPopup("failed_toast", toast) @@ -96,13 +113,18 @@ class ChooseSoundFileViewModel( } } + fun onChooseRingtone(uri: String) { + viewModelScope.launch { + returnResult.update { ActionData.Sound.Ringtone(uri) } + } + } + @Suppress("UNCHECKED_CAST") class Factory( private val resourceProvider: ResourceProvider, private val useCase: ChooseSoundFileUseCase, ) : ViewModelProvider.NewInstanceFactory() { - override fun create(modelClass: Class): T = - ChooseSoundFileViewModel(resourceProvider, useCase) as T + override fun create(modelClass: Class): T = ChooseSoundFileViewModel(resourceProvider, useCase) as T } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt index 612cb913af..340f304358 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt @@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material3.BottomAppBar @@ -27,6 +29,7 @@ import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -41,13 +44,18 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.ui.compose.CheckBoxText import io.github.sds100.keymapper.util.ui.compose.KeyMapperDropdownMenu import io.github.sds100.keymapper.util.ui.compose.SearchAppBarActions +import io.github.sds100.keymapper.util.ui.compose.WindowSizeClassExt.compareTo @Composable fun ChooseElementScreen( @@ -59,8 +67,11 @@ fun ChooseElementScreen( onQueryChange: (String) -> Unit = {}, onClickElement: (Long) -> Unit = {}, onSelectInteractionType: (NodeInteractionType?) -> Unit = {}, + onAdditionalElementsCheckedChange: (Boolean) -> Unit = {}, ) { - var interactionTypeExpanded by rememberSaveable { mutableStateOf(false) } + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val widthSizeClass = windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass + val heightSizeClass = windowAdaptiveInfo.windowSizeClass.windowHeightSizeClass Scaffold( modifier.displayCutoutPadding(), @@ -105,67 +116,137 @@ fun ChooseElementScreen( style = MaterialTheme.typography.titleLarge, ) - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.action_interact_ui_element_choose_element_text), - style = MaterialTheme.typography.bodyMedium, - ) - - Spacer(modifier = Modifier.height(8.dp)) + if (heightSizeClass == WindowHeightSizeClass.COMPACT || widthSizeClass >= WindowWidthSizeClass.EXPANDED) { + Row { + InfoSection( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(1f), + state = state, + onSelectInteractionType = onSelectInteractionType, + onAdditionalElementsCheckedChange = onAdditionalElementsCheckedChange, + ) - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Rounded.ErrorOutline, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, + ListSection( + modifier = Modifier.weight(1f), + state = state, + onClickElement = onClickElement, + ) + } + } else { + InfoSection( + state = state, + onSelectInteractionType = onSelectInteractionType, + onAdditionalElementsCheckedChange = onAdditionalElementsCheckedChange, ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_subtitle), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.titleSmall, + + ListSection( + modifier = Modifier.fillMaxSize(), + state = state, + onClickElement = onClickElement, ) } + } + } + } +} + +@Composable +private fun InfoSection( + modifier: Modifier = Modifier, + state: State, + onSelectInteractionType: (NodeInteractionType?) -> Unit, + onAdditionalElementsCheckedChange: (Boolean) -> Unit, +) { + Column(modifier = modifier) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_choose_element_text), + style = MaterialTheme.typography.bodyMedium, + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_text), - style = MaterialTheme.typography.bodyMedium, - ) - Spacer(modifier = Modifier.height(8.dp)) - - when (state) { - State.Loading -> LoadingList(modifier = Modifier.fillMaxSize()) - is State.Data -> { - val listItems = state.data.listItems - - if (listItems.isEmpty()) { - EmptyList(modifier = Modifier.fillMaxSize()) - } else { - KeyMapperDropdownMenu( - modifier = Modifier.padding(horizontal = 16.dp), - expanded = interactionTypeExpanded, - onExpandedChange = { interactionTypeExpanded = it }, - label = { Text(stringResource(R.string.action_interact_ui_element_filter_interaction_type_dropdown)) }, - values = state.data.interactionTypes, - selectedValue = state.data.selectedInteractionType, - onValueChanged = onSelectInteractionType, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - LoadedList( - modifier = Modifier.fillMaxSize(), - listItems = listItems, - onClick = onClickElement, - ) - } - } + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Rounded.ErrorOutline, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_subtitle), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleSmall, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_text), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (state is State.Data) { + var interactionTypeExpanded by rememberSaveable { mutableStateOf(false) } + + CheckBoxText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + text = stringResource(R.string.action_interact_ui_element_checkbox_additional_elements), + isChecked = state.data.showAdditionalElements, + onCheckedChange = onAdditionalElementsCheckedChange, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + KeyMapperDropdownMenu( + modifier = Modifier.padding(horizontal = 16.dp), + expanded = interactionTypeExpanded, + onExpandedChange = { interactionTypeExpanded = it }, + label = { Text(stringResource(R.string.action_interact_ui_element_filter_interaction_type_dropdown)) }, + values = state.data.interactionTypes, + selectedValue = state.data.selectedInteractionType, + onValueChanged = onSelectInteractionType, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +@Composable +private fun ListSection( + modifier: Modifier = Modifier, + state: State, + onClickElement: (Long) -> Unit, +) { + when (state) { + State.Loading -> LoadingList(modifier = modifier.fillMaxSize()) + is State.Data -> { + val listItems = state.data.listItems + + Column(modifier = modifier) { + if (listItems.isEmpty()) { + EmptyList( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + ) + } else { + LoadedList( + modifier = Modifier.fillMaxSize(), + listItems = listItems, + onClick = onClickElement, + ) } } } @@ -259,6 +340,13 @@ private fun UiElementListItem( ) } + if (model.nodeTooltipHint != null) { + TextWithLeadingLabel( + title = stringResource(R.string.action_interact_ui_element_tooltip_label), + text = model.nodeTooltipHint, + ) + } + if (model.nodeUniqueId != null) { TextWithLeadingLabel( title = stringResource(R.string.action_interact_ui_element_unique_id_label), @@ -309,6 +397,7 @@ private fun Empty() { listItems = emptyList(), interactionTypes = emptyList(), selectedInteractionType = null, + showAdditionalElements = false, ), ), query = "Key Mapper", @@ -327,38 +416,74 @@ private fun Loading() { } } +private val listItems = listOf( + UiElementListItemModel( + id = 1L, + nodeText = "Open Settings", + nodeClassName = "android.widget.ImageButton", + nodeViewResourceId = "menu_button", + nodeUniqueId = "123456789", + nodeTooltipHint = "Open menu", + interactionTypesText = "Tap, Tap and hold, Scroll forward", + interactionTypes = setOf( + NodeInteractionType.CLICK, + NodeInteractionType.LONG_CLICK, + NodeInteractionType.SCROLL_FORWARD, + ), + interacted = true, + ), +) + +private val loadedState = SelectUiElementState( + listItems = listItems, + interactionTypes = listOf( + null to "Any", + NodeInteractionType.CLICK to "Tap", + NodeInteractionType.LONG_CLICK to "Tap and hold", + ), + selectedInteractionType = null, + showAdditionalElements = true, +) + @Preview @Composable -private fun Loaded() { - val listItems = listOf( - UiElementListItemModel( - id = 1L, - nodeText = "Open Settings", - nodeClassName = "android.widget.ImageButton", - nodeViewResourceId = "menu_button", - nodeUniqueId = "123456789", - interactionTypesText = "Tap, Tap and hold, Scroll forward", - interactionTypes = setOf( - NodeInteractionType.CLICK, - NodeInteractionType.LONG_CLICK, - NodeInteractionType.SCROLL_FORWARD, - ), - ), - ) +private fun LoadedPortrait() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Data(loadedState), + query = "Key Mapper", + ) + } +} - val state = SelectUiElementState( - listItems = listItems, - interactionTypes = listOf( - null to "Any", - NodeInteractionType.CLICK to "Tap", - NodeInteractionType.LONG_CLICK to "Tap and hold", - ), - selectedInteractionType = null, - ) +@Preview(widthDp = 800, heightDp = 300) +@Composable +private fun LoadedPhoneLandscape() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Data(loadedState), + query = "Key Mapper", + ) + } +} + +@Preview(device = Devices.TABLET) +@Composable +private fun LoadedTablet() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Data(loadedState), + query = "Key Mapper", + ) + } +} +@Preview(device = Devices.NEXUS_7) +@Composable +private fun LoadedTabletVertical() { KeyMapperTheme { ChooseElementScreen( - state = State.Data(state), + state = State.Data(loadedState), query = "Key Mapper", ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt index ddde228691..c8f0f24c10 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt @@ -4,7 +4,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf @@ -71,7 +78,12 @@ class InteractUiElementFragment : Fragment() { setContent { KeyMapperTheme { InteractUiElementScreen( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ), viewModel = viewModel, navigateBack = findNavController()::navigateUp, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index c4fd683a5d..7ff879f499 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -42,6 +43,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -57,12 +59,15 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme @@ -73,6 +78,7 @@ import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.util.ui.compose.KeyMapperDropdownMenu import io.github.sds100.keymapper.util.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.util.ui.compose.WindowSizeClassExt.compareTo import io.github.sds100.keymapper.util.ui.compose.icons.AdGroup import io.github.sds100.keymapper.util.ui.compose.icons.JumpToElement import io.github.sds100.keymapper.util.ui.compose.icons.KeyMapperIcons @@ -135,7 +141,7 @@ fun InteractUiElementScreen( composable(DEST_SELECT_APP) { ChooseAppScreen( modifier = Modifier.fillMaxSize(), - title = stringResource(R.string.action_interact_ui_element_choose_element_title), + title = stringResource(R.string.action_interact_ui_element_choose_app_title), state = appListState, query = appSearchQuery, onQueryChange = { query -> viewModel.appSearchQuery.update { query } }, @@ -161,6 +167,7 @@ fun InteractUiElementScreen( navController.popBackStack(route = DEST_LANDING, inclusive = false) }, onSelectInteractionType = viewModel::onSelectInteractionTypeFilter, + onAdditionalElementsCheckedChange = viewModel::onAdditionalElementsCheckedChanged, ) } } @@ -180,6 +187,10 @@ private fun LandingScreen( ) { val snackbarHostState = SnackbarHostState() + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val widthSizeClass = windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass + val heightSizeClass = windowAdaptiveInfo.windowSizeClass.windowHeightSizeClass + Scaffold( modifier.displayCutoutPadding(), snackbarHost = { SnackbarHost(snackbarHostState) }, @@ -224,7 +235,7 @@ private fun LandingScreen( end = endPadding, ), ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Column { Text( modifier = Modifier.padding( start = 16.dp, @@ -236,44 +247,91 @@ private fun LandingScreen( style = MaterialTheme.typography.titleLarge, ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - text = stringResource(R.string.action_interact_ui_element_description), - style = MaterialTheme.typography.bodyMedium, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - RecordingSection( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = recordState, - onRecordClick = onRecordClick, - openSelectAppScreen = openSelectAppScreen, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - if (selectedElementState != null) { - HorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - - Spacer(modifier = Modifier.height(8.dp)) - - SelectedElementSection( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = selectedElementState, - onSelectInteractionType = onSelectInteractionType, - onDescriptionChanged = onDescriptionChanged, - ) + if (heightSizeClass == WindowHeightSizeClass.COMPACT || widthSizeClass >= WindowWidthSizeClass.EXPANDED) { + Row { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxHeight() + .weight(1f), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_description), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + RecordingSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = recordState, + onRecordClick = onRecordClick, + openSelectAppScreen = openSelectAppScreen, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + + if (selectedElementState != null) { + SelectedElementSection( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxHeight() + .padding(horizontal = 16.dp) + .weight(1f), + state = selectedElementState, + onSelectInteractionType = onSelectInteractionType, + onDescriptionChanged = onDescriptionChanged, + ) + } + } + } else { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_description), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + RecordingSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = recordState, + onRecordClick = onRecordClick, + openSelectAppScreen = openSelectAppScreen, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (selectedElementState != null) { + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SelectedElementSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = selectedElementState, + onSelectInteractionType = onSelectInteractionType, + onDescriptionChanged = onDescriptionChanged, + ) + } + } } } } @@ -389,7 +447,7 @@ private fun InteractionCountBox( Column(modifier = Modifier.weight(1f)) { Text( pluralStringResource( - R.plurals.action_interact_ui_element_interactions_detected, + R.plurals.action_interact_ui_element_elements_detected, interactionCount, interactionCount, ), @@ -397,7 +455,7 @@ private fun InteractionCountBox( ) Text( - stringResource(R.string.action_interact_ui_element_choose_interaction), + stringResource(R.string.action_interact_ui_element_choose_app_title), style = MaterialTheme.typography.bodyMedium, ) } @@ -474,6 +532,8 @@ private fun SelectedElementSection( value = state.description, onValueChange = onDescriptionChanged, isError = isError, + maxLines = 1, + singleLine = true, supportingText = if (isError) { { Text(stringResource(R.string.error_cant_be_empty)) } } else { @@ -523,9 +583,18 @@ private fun SelectedElementSection( ) Text(text = state.nodeText, style = MaterialTheme.typography.bodyMedium) + Spacer(modifier = Modifier.height(8.dp)) } - Spacer(modifier = Modifier.height(8.dp)) + if (state.nodeToolTipHint != null) { + Text( + text = stringResource(R.string.action_interact_ui_element_tooltip_label), + style = MaterialTheme.typography.titleSmall, + ) + + Text(text = state.nodeToolTipHint, style = MaterialTheme.typography.bodyMedium) + Spacer(modifier = Modifier.height(8.dp)) + } if (state.nodeClassName != null) { Text( @@ -572,7 +641,7 @@ private fun SelectedElementSection( Spacer(modifier = Modifier.height(8.dp)) - KeyMapperDropdownMenu( + KeyMapperDropdownMenu( expanded = interactionTypeExpanded, onExpandedChange = { interactionTypeExpanded = it }, values = state.interactionTypes, @@ -597,35 +666,52 @@ private fun PreviewEmpty() { @Preview @Composable -private fun PreviewSelectedElement() { +private fun PreviewLoading() { + KeyMapperTheme { + LandingScreen( + recordState = State.Loading, + selectedElementState = null, + ) + } +} + +@Composable +private fun selectedUiElementState(): SelectedUiElementState { val appIcon = LocalContext.current.drawable(R.mipmap.ic_launcher_round) + return SelectedUiElementState( + description = "Tap test node", + packageName = "com.example.test", + appName = "Test App", + appIcon = ComposeIconInfo.Drawable(appIcon), + nodeText = "Test Node", + nodeToolTipHint = "Test tooltip", + nodeClassName = "android.widget.ImageButton", + nodeViewResourceId = "io.github.sds100.keymapper:id/menu_button", + nodeUniqueId = "123", + interactionTypes = listOf(NodeInteractionType.LONG_CLICK to "Tap and hold"), + selectedInteraction = NodeInteractionType.LONG_CLICK, + ) +} + +@Preview(device = Devices.PIXEL_7) +@Composable +private fun PreviewSelectedElementPortrait() { KeyMapperTheme { LandingScreen( recordState = State.Data(RecordUiElementState.Recorded(3)), - selectedElementState = SelectedUiElementState( - description = "Tap test node", - packageName = "com.example.test", - appName = "Test App", - appIcon = ComposeIconInfo.Drawable(appIcon), - nodeText = "Test Node", - nodeClassName = "android.widget.ImageButton", - nodeViewResourceId = "io.github.sds100.keymapper:id/menu_button", - nodeUniqueId = "123", - interactionTypes = listOf(NodeInteractionType.LONG_CLICK to "Tap and hold"), - selectedInteraction = NodeInteractionType.LONG_CLICK, - ), + selectedElementState = selectedUiElementState(), ) } } -@Preview +@Preview(widthDp = 800, heightDp = 300) @Composable -private fun PreviewLoading() { +private fun PreviewSelectedElementLandscape() { KeyMapperTheme { LandingScreen( - recordState = State.Loading, - selectedElementState = null, + recordState = State.Data(RecordUiElementState.Recorded(3)), + selectedElementState = selectedUiElementState(), ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index 80d255273b..67dcac391c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -89,6 +90,7 @@ class InteractUiElementViewModel( } }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + private val selectedElementEntity = MutableStateFlow(null) private val _selectedElementState = MutableStateFlow(null) val selectedElementState: StateFlow = _selectedElementState.asStateFlow() @@ -117,6 +119,15 @@ class InteractUiElementViewModel( private val interactionsByPackage: StateFlow>> = selectedApp .filterNotNull() .flatMapLatest { packageName -> useCase.getInteractionsByPackage(packageName) } + .onEach { state -> + // Automatically show additional elements if no elements that were interacted with + // were detected. + state.ifIsData { list -> + if (list.count { it.interacted } == 0) { + showAdditionalElements.update { true } + } + } + } .stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) private val elementListItems: Flow>> = interactionsByPackage @@ -139,13 +150,20 @@ class InteractUiElementViewModel( private val selectedInteractionTypeFilter = MutableStateFlow(null) + private val showAdditionalElements: MutableStateFlow = MutableStateFlow(false) + private val filteredElementListItems = combine( elementListItems, elementSearchQuery, selectedInteractionTypeFilter, - ) { state, query, interactionType -> + showAdditionalElements, + ) { state, query, interactionType, showAdditionalElements -> state.mapData { listItems -> listItems.filter { model -> + if (!showAdditionalElements && !model.interacted) { + return@filter false + } + if (interactionType != null && !model.interactionTypes.contains(interactionType)) { return@filter false } @@ -153,6 +171,8 @@ class InteractUiElementViewModel( val modelString = buildString { append(model.nodeText) append(" ") + append(model.nodeTooltipHint) + append(" ") append(model.nodeClassName) append(" ") append(model.nodeViewResourceId) @@ -166,7 +186,8 @@ class InteractUiElementViewModel( filteredElementListItems, interactionTypesFilterItems, selectedInteractionTypeFilter, - ) { listItemsState, interactionTypesState, selectedInteractionType -> + showAdditionalElements, + ) { listItemsState, interactionTypesState, selectedInteractionType, showAdditionalElements -> val listItems = listItemsState.dataOrNull() ?: return@combine State.Loading val interactionTypes = interactionTypesState.dataOrNull() ?: return@combine State.Loading @@ -174,6 +195,7 @@ class InteractUiElementViewModel( listItems = listItems, interactionTypes = interactionTypes, selectedInteractionType = selectedInteractionType, + showAdditionalElements = showAdditionalElements, ) State.Data(newState) }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) @@ -189,6 +211,7 @@ class InteractUiElementViewModel( appName = appName, appIcon = appIcon, nodeText = action.text ?: action.contentDescription, + nodeToolTipHint = action.tooltip ?: action.hint, nodeClassName = action.className, nodeViewResourceId = action.viewResourceId, nodeUniqueId = action.uniqueId, @@ -202,7 +225,9 @@ class InteractUiElementViewModel( fun onDoneClick() { val selectedElementState = _selectedElementState.value - if (selectedElementState == null) { + val selectedElementEntity = selectedElementEntity.value + + if (selectedElementState == null || selectedElementEntity == null) { return } @@ -213,13 +238,15 @@ class InteractUiElementViewModel( val action = ActionData.InteractUiElement( description = selectedElementState.description, nodeAction = selectedElementState.selectedInteraction, - packageName = selectedElementState.packageName, - text = selectedElementState.nodeText, - contentDescription = selectedElementState.nodeText, - className = selectedElementState.nodeClassName, - viewResourceId = selectedElementState.nodeViewResourceId, - uniqueId = selectedElementState.nodeUniqueId, - nodeActions = selectedElementState.interactionTypes.map { it.first }.toSet(), + packageName = selectedElementEntity.packageName, + text = selectedElementEntity.text, + contentDescription = selectedElementEntity.contentDescription, + tooltip = selectedElementEntity.tooltip, + hint = selectedElementEntity.hint, + className = selectedElementEntity.className, + viewResourceId = selectedElementEntity.viewResourceId, + uniqueId = selectedElementEntity.uniqueId, + nodeActions = selectedElementEntity.actions, ) viewModelScope.launch { @@ -241,6 +268,11 @@ class InteractUiElementViewModel( fun onSelectApp(packageName: String) { elementSearchQuery.update { null } + + if (packageName != selectedApp.value) { + showAdditionalElements.update { false } + } + selectedApp.update { packageName } } @@ -254,20 +286,32 @@ class InteractUiElementViewModel( val selectedInteraction = NodeInteractionType.entries.first { interaction.actions.contains(it) } + val interactionText = getInteractionTypeString(selectedInteraction) + val descriptionElement = + interaction.text ?: interaction.contentDescription ?: interaction.tooltip + ?: interaction.hint ?: interaction.viewResourceId + + val description = if (descriptionElement == null) { + "" + } else { + "$interactionText: $descriptionElement" + } val newState = SelectedUiElementState( - description = "", + description = description, packageName = interaction.packageName, appName = appName, appIcon = appIcon, nodeText = interaction.text ?: interaction.contentDescription, nodeClassName = interaction.className, + nodeToolTipHint = interaction.tooltip ?: interaction.hint, nodeViewResourceId = interaction.viewResourceId, nodeUniqueId = interaction.uniqueId, interactionTypes = buildInteractionTypeFilterItems(interaction.actions), selectedInteraction = selectedInteraction, ) + selectedElementEntity.update { interaction } _selectedElementState.update { newState } } } @@ -288,6 +332,10 @@ class InteractUiElementViewModel( } } + fun onAdditionalElementsCheckedChanged(checked: Boolean) { + showAdditionalElements.update { checked } + } + private suspend fun startRecording() { useCase.startRecording().onFailure { error -> if (error == Error.AccessibilityServiceDisabled) { @@ -325,9 +373,11 @@ class InteractUiElementViewModel( nodeViewResourceId = resourceIdText, nodeText = node.text ?: node.contentDescription, nodeClassName = node.className, - nodeUniqueId = node.uniqueId?.toString(), + nodeUniqueId = node.uniqueId, + nodeTooltipHint = node.tooltip ?: node.hint, interactionTypesText = node.actions.joinToString { getInteractionTypeString(it) }, interactionTypes = node.actions, + interacted = node.interacted, ) } @@ -352,7 +402,6 @@ class InteractUiElementViewModel( NodeInteractionType.CLICK -> getString(R.string.action_interact_ui_element_interaction_type_click) NodeInteractionType.LONG_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_long_click) NodeInteractionType.FOCUS -> getString(R.string.action_interact_ui_element_interaction_type_focus) - NodeInteractionType.SELECT -> getString(R.string.action_interact_ui_element_interaction_type_select) NodeInteractionType.SCROLL_FORWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_forward) NodeInteractionType.SCROLL_BACKWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_backward) NodeInteractionType.EXPAND -> getString(R.string.action_interact_ui_element_interaction_type_expand) @@ -377,6 +426,7 @@ data class SelectedUiElementState( val appName: String, val appIcon: ComposeIconInfo.Drawable?, val nodeText: String?, + val nodeToolTipHint: String?, val nodeClassName: String?, val nodeViewResourceId: String?, val nodeUniqueId: String?, @@ -399,14 +449,20 @@ data class SelectUiElementState( val listItems: List, val interactionTypes: List>, val selectedInteractionType: NodeInteractionType?, + val showAdditionalElements: Boolean, ) data class UiElementListItemModel( val id: Long, val nodeViewResourceId: String?, val nodeText: String?, + val nodeTooltipHint: String?, val nodeClassName: String?, val nodeUniqueId: String?, val interactionTypesText: String, val interactionTypes: Set, + /** + * Whether the user interacted with this element. + */ + val interacted: Boolean, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt index f31a6520f5..8d02874b45 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt @@ -6,7 +6,6 @@ enum class NodeInteractionType(val accessibilityActionId: Int) { CLICK(AccessibilityNodeInfo.ACTION_CLICK), LONG_CLICK(AccessibilityNodeInfo.ACTION_LONG_CLICK), FOCUS(AccessibilityNodeInfo.ACTION_FOCUS), - SELECT(AccessibilityNodeInfo.ACTION_SELECT), SCROLL_FORWARD(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD), SCROLL_BACKWARD(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD), EXPAND(AccessibilityNodeInfo.ACTION_EXPAND), 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 f2e22b2ce8..bb82002476 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 @@ -44,7 +44,7 @@ import io.github.sds100.keymapper.data.repositories.FloatingLayoutRepository import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.data.repositories.RepositoryUtils -import io.github.sds100.keymapper.mappings.keymaps.KeyMapRepository +import io.github.sds100.keymapper.keymaps.KeyMapRepository import io.github.sds100.keymapper.system.files.FileAdapter import io.github.sds100.keymapper.system.files.IFile import io.github.sds100.keymapper.util.DefaultDispatcherProvider @@ -54,7 +54,9 @@ import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.Success +import io.github.sds100.keymapper.util.TreeNode import io.github.sds100.keymapper.util.UuidGenerator +import io.github.sds100.keymapper.util.breadFirstTraversal import io.github.sds100.keymapper.util.onFailure import io.github.sds100.keymapper.util.then import kotlinx.coroutines.CoroutineScope @@ -252,6 +254,9 @@ class BackupManagerImpl( // Do nothing. Just added the accessibility node table. JsonMigration(18, 19) { json -> json }, + + // Do nothing. Just added columns to the accessibility node table. + JsonMigration(19, 20) { json -> json }, ) if (keyMapListJsonArray != null) { @@ -470,55 +475,11 @@ class BackupManagerImpl( // Group parents must be restored first so an SqliteConstraintException // is not thrown when restoring a child group. - val groupsToRestoreMap = backupContent.groups.associateBy { it.uid }.toMutableMap() - val groupRestoreQueue = LinkedList() - - // Order the groups into a queue such that a parent is always before a child. - for (group in backupContent.groups) { - if (groupsToRestoreMap.containsKey(group.uid)) { - groupRestoreQueue.addFirst(group) - } + val groupRestoreTrees = buildGroupTrees(backupContent.groups) - var parent = groupsToRestoreMap[group.parentUid] - - while (parent != null) { - groupRestoreQueue.addFirst(parent) - groupsToRestoreMap.remove(parent.uid) - parent = groupsToRestoreMap[parent.parentUid] - } - } - - for (group in groupRestoreQueue) { - // Set the last opened date to now so that the imported group - // shows as the most recent. - var modifiedGroup = group.copy(lastOpenedDate = currentTime) - - // If the group's parent wasn't backed up or doesn't exist - // then set it the parent to the root group - if (!groupUids.contains(group.parentUid)) { - modifiedGroup = modifiedGroup.copy(parentUid = null) - } - - val siblings = - groupRepository.getGroupsByParent(modifiedGroup.parentUid).first() - - modifiedGroup = RepositoryUtils.saveUniqueName( - modifiedGroup, - saveBlock = { renamedGroup -> - // Do not rename the group with a (1) if it is the same UID. Just overwrite the name. - if (siblings.any { sibling -> sibling.uid != renamedGroup.uid && sibling.name == renamedGroup.name }) { - throw IllegalStateException("Non unique group name") - } - }, - renameBlock = { entity, suffix -> - entity.copy(name = "${entity.name} $suffix") - }, - ) - - if (existingGroupUids.contains(modifiedGroup.uid)) { - groupRepository.update(modifiedGroup) - } else { - groupRepository.insert(modifiedGroup) + for (tree in groupRestoreTrees) { + tree.breadFirstTraversal { group -> + restoreGroup(group, currentTime, groupUids, existingGroupUids) } } } @@ -612,6 +573,91 @@ class BackupManagerImpl( } } + private suspend fun restoreGroup( + group: GroupEntity, + currentTime: Long, + groupUids: Set, + existingGroupUids: Set, + ) { + // Set the last opened date to now so that the imported group + // shows as the most recent. + var modifiedGroup = group.copy(lastOpenedDate = currentTime) + + // If the group's parent wasn't backed up or doesn't exist + // then set it the parent to the root group + if (!groupUids.contains(group.parentUid)) { + modifiedGroup = modifiedGroup.copy(parentUid = null) + } + + val siblings = + groupRepository.getGroupsByParent(modifiedGroup.parentUid).first() + + modifiedGroup = RepositoryUtils.saveUniqueName( + modifiedGroup, + saveBlock = { renamedGroup -> + // Do not rename the group with a (1) if it is the same UID. Just overwrite the name. + if (siblings.any { sibling -> sibling.uid != renamedGroup.uid && sibling.name == renamedGroup.name }) { + throw IllegalStateException("Non unique group name") + } + }, + renameBlock = { entity, suffix -> + entity.copy(name = "${entity.name} $suffix") + }, + ) + + if (existingGroupUids.contains(modifiedGroup.uid)) { + groupRepository.update(modifiedGroup) + } else { + groupRepository.insert(modifiedGroup) + } + } + + /** + * Converts the group relationships into trees. This first finds all the root groups which + * have no parent. Then it loops over all the other groups indefinitely until they have been + * added to their parent. If the parent does not exist while looping then it is skipped and + * processed in the next iteration. + * + * @return A list of the root nodes for all the group trees. + */ + private fun buildGroupTrees(groups: List): List> { + if (groups.isEmpty()) { + return emptyList() + } + + val nodeMap = mutableMapOf>() + val rootNodes = mutableListOf>() + + val groupQueue = LinkedList() + + for (group in groups) { + if (group.parentUid == null) { + val node = TreeNode(group) + nodeMap[group.uid] = node + rootNodes.add(node) + } else { + groupQueue.add(group) + } + } + + while (groupQueue.isNotEmpty()) { + val groupsToRemove = mutableListOf() + + for (group in groupQueue) { + if (nodeMap.containsKey(group.parentUid)) { + val node = TreeNode(group) + nodeMap[group.uid] = node + nodeMap[group.parentUid]!!.children.add(node) + groupsToRemove.add(group) + } + } + + groupQueue.removeAll(groupsToRemove.toSet()) + } + + return rootNodes + } + private suspend fun appendKeyMapsInRepository(keyMaps: List) = withContext(dispatchers.default()) { val randomUids = keyMaps.map { it.copy(uid = UUID.randomUUID().toString()) } keyMapRepository.insert(*randomUids.toTypedArray()) 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 index 6d677359e4..10adb36631 100644 --- a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt +++ b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeColors.kt @@ -45,6 +45,10 @@ object ComposeColors { val onGreenLight = Color(0xFFFFFFFF) val greenContainerLight = Color(0xFFBCF0B4) val onGreenContainerLight = Color(0xFF235024) + val magiskTealLight = Color(0xFF008072) + val onMagiskTealLight = Color(0xFFFFFFFF) + val shizukuBlueLight = Color(0xFF4556B7) + val onShizukuBlueLight = Color(0xFFFFFFFF) val primaryDark = Color(0xFFAAC7FF) val onPrimaryDark = Color(0xFF0A305F) @@ -87,4 +91,8 @@ object ComposeColors { val onGreenDark = Color(0xFF0A390F) val greenContainerDark = Color(0xFF235024) val onGreenContainerDark = Color(0xFFBCF0B4) + val magiskTealDark = Color(0xFF009B8C) + val onMagiskTealDark = Color(0xFFFFFFFF) + val shizukuBlueDark = Color(0xFFB7C4F4) + val onShizukuBlueDark = Color(0xFF0A305F) } 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 index 2c1000f6bf..d8c26c3b59 100644 --- a/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt +++ b/app/src/main/java/io/github/sds100/keymapper/compose/ComposeCustomColors.kt @@ -1,6 +1,10 @@ package io.github.sds100.keymapper.compose +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color /** @@ -17,6 +21,10 @@ data class ComposeCustomColors( val onGreen: Color = Color.Unspecified, val greenContainer: Color = Color.Unspecified, val onGreenContainer: Color = Color.Unspecified, + val magiskTeal: Color = Color.Unspecified, + val onMagiskTeal: Color = Color.Unspecified, + val shizukuBlue: Color = Color.Unspecified, + val onShizukuBlue: Color = Color.Unspecified, ) { companion object { val LightPalette = ComposeCustomColors( @@ -26,6 +34,11 @@ data class ComposeCustomColors( onGreen = ComposeColors.onGreenLight, greenContainer = ComposeColors.greenContainerLight, onGreenContainer = ComposeColors.onGreenContainerLight, + magiskTeal = ComposeColors.magiskTealLight, + onMagiskTeal = ComposeColors.onMagiskTealLight, + shizukuBlue = ComposeColors.shizukuBlueLight, + onShizukuBlue = ComposeColors.onShizukuBlueLight, + ) val DarkPalette = ComposeCustomColors( @@ -35,6 +48,23 @@ data class ComposeCustomColors( onGreen = ComposeColors.onGreenDark, greenContainer = ComposeColors.greenContainerDark, onGreenContainer = ComposeColors.onGreenContainerDark, + magiskTeal = ComposeColors.magiskTealDark, + onMagiskTeal = ComposeColors.onMagiskTealDark, + shizukuBlue = ComposeColors.shizukuBlueDark, + onShizukuBlue = ComposeColors.onShizukuBlueDark, ) } + + @Composable + @Stable + fun contentColorFor(color: Color): Color { + return when (color) { + red -> onRed + green -> onGreen + greenContainer -> onGreenContainer + magiskTeal -> onMagiskTeal + shizukuBlue -> onShizukuBlue + else -> MaterialTheme.colorScheme.contentColorFor(color) + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintFragment.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintFragment.kt index 4d694b4b6b..727bd91534 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintFragment.kt @@ -4,7 +4,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf @@ -25,7 +32,6 @@ import io.github.sds100.keymapper.util.ui.showPopups import io.github.sds100.keymapper.util.viewLifecycleScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json class ChooseConstraintFragment : Fragment() { @@ -73,7 +79,12 @@ class ChooseConstraintFragment : Fragment() { setContent { KeyMapperTheme { ChooseConstraintScreen( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ), viewModel = viewModel, onNavigateBack = findNavController()::navigateUp, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintScreen.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintScreen.kt index 6ed94f2c43..958b557d29 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintScreen.kt @@ -49,7 +49,7 @@ import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo -import io.github.sds100.keymapper.util.ui.compose.SimpleListItem +import io.github.sds100.keymapper.util.ui.compose.SimpleListItemFixedHeight import io.github.sds100.keymapper.util.ui.compose.SimpleListItemModel import kotlinx.coroutines.flow.update @@ -246,7 +246,7 @@ private fun ListScreen( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { items(listItems, key = { it.id }) { model -> - SimpleListItem( + SimpleListItemFixedHeight( modifier = Modifier.fillMaxWidth(), model = model, onClick = { onClickAction(model.id) }, @@ -291,12 +291,12 @@ private fun PreviewGrid() { state = State.Data( listOf( SimpleListItemModel( - "app", + "app1", title = "App in foreground", icon = ComposeIconInfo.Vector(Icons.Rounded.Android), ), SimpleListItemModel( - "app", + "app2", title = "App not in foreground", icon = ComposeIconInfo.Vector(Icons.Rounded.Android), subtitle = "Error", diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConfigConstraintsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConfigConstraintsViewModel.kt index 534a4c4978..f4512555ab 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConfigConstraintsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConfigConstraintsViewModel.kt @@ -3,8 +3,8 @@ package io.github.sds100.keymapper.constraints import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel +import io.github.sds100.keymapper.keymaps.ConfigKeyMapUseCase +import io.github.sds100.keymapper.keymaps.ShortcutModel import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.State diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintsScreen.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintsScreen.kt index bb2b78a354..8c5d50b219 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintsScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintsScreen.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel -import io.github.sds100.keymapper.mappings.keymaps.ShortcutRow +import io.github.sds100.keymapper.keymaps.ShortcutModel +import io.github.sds100.keymapper.keymaps.ShortcutRow import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.drawable 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 06ca3f1fec..0daba2d08a 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 @@ -10,7 +10,10 @@ import androidx.datastore.preferences.core.stringSetPreferencesKey */ object Keys { val darkTheme = stringPreferencesKey("pref_dark_theme_mode") + + @Deprecated("Now use the libsu library to detect whether the device is rooted.") val hasRootPermission = booleanPreferencesKey("pref_allow_root_features") + val shownAppIntro = booleanPreferencesKey("pref_first_time") val showImePickerNotification = booleanPreferencesKey("pref_show_ime_notification") val showToggleKeyMapsNotification = booleanPreferencesKey("pref_show_remappings_notification") @@ -59,8 +62,6 @@ object Keys { val lastInstalledVersionCodeBackground = intPreferencesKey("last_installed_version_accessibility_service") - val shownQuickStartGuideHint = booleanPreferencesKey("tap_target_quick_start_guide") - val fingerprintGesturesAvailable = booleanPreferencesKey("fingerprint_gestures_available") @@ -93,4 +94,28 @@ object Keys { * Whether the user viewed the advanced triggers. */ val viewedAdvancedTriggers = booleanPreferencesKey("key_viewed_advanced_triggers") + + val neverShowNotificationPermissionAlert = + booleanPreferencesKey("key_never_show_notification_permission_alert") + + val shownTapTargetCreateKeyMap = + booleanPreferencesKey("key_shown_tap_target_create_key_map") + + val shownTapTargetRecordTrigger = + booleanPreferencesKey("key_shown_tap_target_record_trigger") + + val shownTapTargetAdvancedTriggers = + booleanPreferencesKey("key_shown_tap_target_advanced_triggers") + + val shownTapTargetChooseAction = + booleanPreferencesKey("key_shown_tap_target_choose_action") + + val shownTapTargetChooseConstraint = + booleanPreferencesKey("key_shown_tap_target_choose_constraint") + + val skipTapTargetTutorial = + booleanPreferencesKey("key_skip_tap_target_tutorial") + + val isProModeWarningUnderstood = + booleanPreferencesKey("key_is_pro_mode_warning_understood") } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt index e4644b1055..bba74e2b66 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt @@ -32,6 +32,7 @@ import io.github.sds100.keymapper.data.migration.AutoMigration14To15 import io.github.sds100.keymapper.data.migration.AutoMigration15To16 import io.github.sds100.keymapper.data.migration.AutoMigration16To17 import io.github.sds100.keymapper.data.migration.AutoMigration18To19 +import io.github.sds100.keymapper.data.migration.AutoMigration19To20 import io.github.sds100.keymapper.data.migration.Migration10To11 import io.github.sds100.keymapper.data.migration.Migration11To12 import io.github.sds100.keymapper.data.migration.Migration13To14 @@ -60,6 +61,8 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class), // Adds accessibility node table AutoMigration(from = 18, to = 19, spec = AutoMigration18To19::class), + // Adds interacted, tooltip, and hint fields to accessibility node entity + AutoMigration(from = 19, to = 20, spec = AutoMigration19To20::class), ], ) @TypeConverters( @@ -72,7 +75,7 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 abstract class AppDatabase : RoomDatabase() { companion object { const val DATABASE_NAME = "key_map_database" - const val DATABASE_VERSION = 19 + const val DATABASE_VERSION = 20 val MIGRATION_1_2 = object : Migration(1, 2) { diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt index ed2bd36250..1aa13584a5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt @@ -19,6 +19,9 @@ interface AccessibilityNodeDao { const val KEY_VIEW_RESOURCE_ID = "view_resource_id" const val KEY_UNIQUE_ID = "unique_id" const val KEY_ACTIONS = "actions" + const val KEY_INTERACTED = "interacted" + const val KEY_TOOLTIP = "tooltip" + const val KEY_HINT = "hint" } @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_ID = (:id)") diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt index b5f7481efc..c72b0a0f78 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/KeyMapDao.kt @@ -70,4 +70,7 @@ interface KeyMapDao { @Update(onConflict = OnConflictStrategy.ABORT) suspend fun update(vararg keyMap: KeyMapEntity) + + @Query("SELECT COUNT(*) FROM $TABLE_NAME") + fun count(): Flow } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt index ca3ba0ee1c..f84769395c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -8,9 +8,12 @@ import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_ACTIONS import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_CLASS_NAME import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_CONTENT_DESCRIPTION +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_HINT import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_ID +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_INTERACTED import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_PACKAGE_NAME import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_TEXT +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_TOOLTIP import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_UNIQUE_ID import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_VIEW_RESOURCE_ID import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.TABLE_NAME @@ -45,4 +48,16 @@ data class AccessibilityNodeEntity( @ColumnInfo(name = KEY_ACTIONS) val actions: Set, + + /** + * Whether the user interacted with this node. + */ + @ColumnInfo(name = KEY_INTERACTED, defaultValue = false.toString()) + val interacted: Boolean, + + @ColumnInfo(name = KEY_TOOLTIP, defaultValue = "NULL") + val tooltip: String?, + + @ColumnInfo(name = KEY_HINT, defaultValue = "NULL") + val hint: String?, ) : Parcelable 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 4385e0b3a4..be62380df3 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 @@ -95,6 +95,8 @@ data class ActionEntity( const val EXTRA_ACCESSIBILITY_CONTENT_DESCRIPTION = "extra_accessibility_content_description" const val EXTRA_ACCESSIBILITY_TEXT = "extra_accessibility_text" + const val EXTRA_ACCESSIBILITY_TOOLTIP = "extra_accessibility_tooltip" + const val EXTRA_ACCESSIBILITY_HINT = "extra_accessibility_hint" const val EXTRA_ACCESSIBILITY_CLASS_NAME = "extra_accessibility_class_name" const val EXTRA_ACCESSIBILITY_VIEW_RESOURCE_ID = "extra_accessibility_view_resource_id" const val EXTRA_ACCESSIBILITY_UNIQUE_ID = "extra_accessibility_unique_id" diff --git a/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration19To20.kt b/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration19To20.kt new file mode 100644 index 0000000000..edec936175 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration19To20.kt @@ -0,0 +1,5 @@ +package io.github.sds100.keymapper.data.migration + +import androidx.room.migration.AutoMigrationSpec + +class AutoMigration19To20 : AutoMigrationSpec 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 2a0f1737a2..7ad76800a7 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 @@ -5,7 +5,7 @@ import io.github.sds100.keymapper.data.db.dao.KeyMapDao import io.github.sds100.keymapper.data.entities.FingerprintMapEntity import io.github.sds100.keymapper.data.entities.KeyMapEntity import io.github.sds100.keymapper.data.migration.fingerprintmaps.FingerprintToKeyMapMigration -import io.github.sds100.keymapper.mappings.keymaps.KeyMapRepository +import io.github.sds100.keymapper.keymaps.KeyMapRepository import io.github.sds100.keymapper.util.DefaultDispatcherProvider import io.github.sds100.keymapper.util.DispatcherProvider import io.github.sds100.keymapper.util.State @@ -122,6 +122,10 @@ class RoomKeyMapRepository( } } + override fun count(): Flow { + return keyMapDao.count().flowOn(dispatchers.io()) + } + private suspend fun migrateFingerprintMaps() = withContext(dispatchers.io()) { val entities = fingerprintMapDao.getAll().first() 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 52b2a6f59d..5ba4a8c508 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 @@ -4,6 +4,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -58,6 +66,11 @@ class HomeFragment : Fragment() { setContent { KeyMapperTheme { HomeScreen( + modifier = Modifier + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ), viewModel = homeViewModel, onSettingsClick = { findNavController().navigate(NavAppDirections.toSettingsFragment()) diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeKeyMapListScreen.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeKeyMapListScreen.kt index dc46702f69..c7f448d788 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/HomeKeyMapListScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeKeyMapListScreen.kt @@ -52,20 +52,22 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.canopas.lib.showcase.IntroShowcase import io.github.sds100.keymapper.R import io.github.sds100.keymapper.backup.ImportExportState import io.github.sds100.keymapper.backup.RestoreType import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.constraints.ConstraintMode import io.github.sds100.keymapper.groups.GroupListItemModel -import io.github.sds100.keymapper.mappings.keymaps.KeyMapAppBarState -import io.github.sds100.keymapper.mappings.keymaps.KeyMapList -import io.github.sds100.keymapper.mappings.keymaps.KeyMapListViewModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.DpadTriggerSetupBottomSheet -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapListItemModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError +import io.github.sds100.keymapper.keymaps.KeyMapAppBarState +import io.github.sds100.keymapper.keymaps.KeyMapList +import io.github.sds100.keymapper.keymaps.KeyMapListViewModel +import io.github.sds100.keymapper.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.sorting.SortBottomSheet import io.github.sds100.keymapper.system.files.FileUtils +import io.github.sds100.keymapper.trigger.DpadTriggerSetupBottomSheet +import io.github.sds100.keymapper.trigger.KeyMapListItemModel +import io.github.sds100.keymapper.trigger.TriggerError import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.ShareUtils import io.github.sds100.keymapper.util.State @@ -73,6 +75,8 @@ import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.CollapsableFloatingActionButton import io.github.sds100.keymapper.util.ui.compose.ComposeChipModel import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.KeyMapperTapTarget +import io.github.sds100.keymapper.util.ui.compose.keyMapperShowcaseStyle import io.github.sds100.keymapper.util.ui.compose.openUriSafe @OptIn(ExperimentalMaterial3Api::class) @@ -151,107 +155,124 @@ fun HomeKeyMapListScreen( var keyMapListBottomPadding by remember { mutableStateOf(100.dp) } - HomeKeyMapListScreen( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - snackbarState = snackbarState, - floatingActionButton = { - AnimatedVisibility( - state.appBarState !is KeyMapAppBarState.Selecting, - enter = fadeIn() + slideInHorizontally(initialOffsetX = { it }), - exit = fadeOut() + slideOutHorizontally(targetOffsetX = { it }), - ) { - CollapsableFloatingActionButton( - modifier = Modifier - .padding(bottom = fabBottomPadding) - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.End)), - onClick = viewModel::onNewKeyMapClick, - showText = viewModel.showFabText, - text = stringResource(R.string.home_fab_new_key_map), - ) - } - }, - listContent = { - KeyMapList( - modifier = Modifier.animateContentSize(), - lazyListState = rememberLazyListState(), - listItems = state.listItems, - footerText = stringResource(R.string.home_key_map_list_footer_text), - isSelectable = state.appBarState is KeyMapAppBarState.Selecting, - onClickKeyMap = viewModel::onKeyMapCardClick, - onLongClickKeyMap = viewModel::onKeyMapCardLongClick, - onSelectedChange = viewModel::onKeyMapSelectedChanged, - onFixClick = viewModel::onFixClick, - onTriggerErrorClick = viewModel::onFixTriggerError, - bottomListPadding = keyMapListBottomPadding, - ) + IntroShowcase( + showIntroShowCase = state.showCreateKeyMapTapTarget, + onShowCaseCompleted = { + viewModel.onTapTargetsCompleted() }, - appBarContent = { - KeyMapListAppBar( - state = state.appBarState, - scrollBehavior = scrollBehavior, - onSettingsClick = onSettingsClick, - onAboutClick = onAboutClick, - onSortClick = { viewModel.showSortBottomSheet = true }, - onHelpClick = { uriHandler.openUriSafe(ctx, helpUrl) }, - onExportClick = viewModel::onExportClick, - onImportClick = { importFileLauncher.launch(FileUtils.MIME_TYPE_ALL) }, - onInputMethodPickerClick = viewModel::showInputMethodPicker, - onTogglePausedClick = viewModel::onTogglePausedClick, - onFixWarningClick = viewModel::onFixWarningClick, - onBackClick = { - if (!viewModel.onBackClick()) { - finishActivity() - } - }, - onSelectAllClick = viewModel::onSelectAllClick, - onNewGroupClick = viewModel::onNewGroupClick, - onRenameGroupClick = viewModel::onRenameGroupClick, - onEditGroupNameClick = viewModel::onEditGroupNameClick, - onGroupClick = viewModel::onGroupClick, - onDeleteGroupClick = viewModel::onDeleteGroupClick, - onNewConstraintClick = viewModel::onNewGroupConstraintClick, - onRemoveConstraintClick = viewModel::onRemoveGroupConstraintClick, - onConstraintModeChanged = viewModel::onGroupConstraintModeChanged, - onFixConstraintClick = viewModel::onFixClick, - ) - }, - selectionBottomSheet = { - AnimatedVisibility( - visible = state.appBarState is KeyMapAppBarState.Selecting, - enter = slideInVertically { it }, - exit = slideOutVertically { it }, - ) { - val selectionState = (state.appBarState as? KeyMapAppBarState.Selecting) - ?: KeyMapAppBarState.Selecting( - selectionCount = 0, - selectedKeyMapsEnabled = SelectedKeyMapsEnabled.NONE, - isAllSelected = false, - groups = emptyList(), - breadcrumbs = emptyList(), - showThisGroup = false, + dismissOnClickOutside = true, + ) { + HomeKeyMapListScreen( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarState = snackbarState, + floatingActionButton = { + AnimatedVisibility( + state.appBarState !is KeyMapAppBarState.Selecting, + enter = fadeIn() + slideInHorizontally(initialOffsetX = { it }), + exit = fadeOut() + slideOutHorizontally(targetOffsetX = { it }), + ) { + CollapsableFloatingActionButton( + modifier = Modifier + .padding(bottom = fabBottomPadding) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.End)) + .introShowCaseTarget( + index = 0, + style = keyMapperShowcaseStyle(), + ) { + KeyMapperTapTarget( + OnboardingTapTarget.CREATE_KEY_MAP, + onSkipClick = viewModel::onSkipTapTargetClick, + ) + }, + onClick = viewModel::onNewKeyMapClick, + showText = viewModel.showFabText, + text = stringResource(R.string.home_fab_new_key_map), ) - - SelectionBottomSheet( - modifier = Modifier.onSizeChanged { size -> - keyMapListBottomPadding = - ((size.height.dp / 2) - 100.dp).coerceAtLeast(0.dp) + } + }, + listContent = { + KeyMapList( + modifier = Modifier.animateContentSize(), + lazyListState = rememberLazyListState(), + listItems = state.listItems, + footerText = stringResource(R.string.home_key_map_list_footer_text), + isSelectable = state.appBarState is KeyMapAppBarState.Selecting, + onClickKeyMap = viewModel::onKeyMapCardClick, + onLongClickKeyMap = viewModel::onKeyMapCardLongClick, + onSelectedChange = viewModel::onKeyMapSelectedChanged, + onFixClick = viewModel::onFixClick, + onTriggerErrorClick = viewModel::onFixTriggerError, + bottomListPadding = keyMapListBottomPadding, + ) + }, + appBarContent = { + KeyMapListAppBar( + state = state.appBarState, + scrollBehavior = scrollBehavior, + onSettingsClick = onSettingsClick, + onAboutClick = onAboutClick, + onSortClick = { viewModel.showSortBottomSheet = true }, + onHelpClick = { uriHandler.openUriSafe(ctx, helpUrl) }, + onExportClick = viewModel::onExportClick, + onImportClick = { importFileLauncher.launch(FileUtils.MIME_TYPE_ALL) }, + onInputMethodPickerClick = viewModel::showInputMethodPicker, + onTogglePausedClick = viewModel::onTogglePausedClick, + onFixWarningClick = viewModel::onFixWarningClick, + onBackClick = { + if (!viewModel.onBackClick()) { + finishActivity() + } }, - enabled = selectionState.selectionCount > 0, - groups = selectionState.groups, - breadcrumbs = selectionState.breadcrumbs, - selectedKeyMapsEnabled = selectionState.selectedKeyMapsEnabled, - onEnabledKeyMapsChange = viewModel::onEnabledKeyMapsChange, - onDuplicateClick = viewModel::onDuplicateSelectedKeyMapsClick, - onExportClick = viewModel::onExportSelectedKeyMaps, - onDeleteClick = { showDeleteDialog = true }, - onGroupClick = viewModel::onSelectionGroupClick, + onSelectAllClick = viewModel::onSelectAllClick, onNewGroupClick = viewModel::onNewGroupClick, - showThisGroup = selectionState.showThisGroup, - onThisGroupClick = viewModel::onMoveToThisGroupClick, + onRenameGroupClick = viewModel::onRenameGroupClick, + onEditGroupNameClick = viewModel::onEditGroupNameClick, + onGroupClick = viewModel::onGroupClick, + onDeleteGroupClick = viewModel::onDeleteGroupClick, + onNewConstraintClick = viewModel::onNewGroupConstraintClick, + onRemoveConstraintClick = viewModel::onRemoveGroupConstraintClick, + onConstraintModeChanged = viewModel::onGroupConstraintModeChanged, + onFixConstraintClick = viewModel::onFixClick, ) - } - }, - ) + }, + selectionBottomSheet = { + AnimatedVisibility( + visible = state.appBarState is KeyMapAppBarState.Selecting, + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + ) { + val selectionState = (state.appBarState as? KeyMapAppBarState.Selecting) + ?: KeyMapAppBarState.Selecting( + selectionCount = 0, + selectedKeyMapsEnabled = SelectedKeyMapsEnabled.NONE, + isAllSelected = false, + groups = emptyList(), + breadcrumbs = emptyList(), + showThisGroup = false, + ) + + SelectionBottomSheet( + modifier = Modifier.onSizeChanged { size -> + keyMapListBottomPadding = + ((size.height.dp / 2) - 100.dp).coerceAtLeast(0.dp) + }, + enabled = selectionState.selectionCount > 0, + groups = selectionState.groups, + breadcrumbs = selectionState.breadcrumbs, + selectedKeyMapsEnabled = selectionState.selectedKeyMapsEnabled, + onEnabledKeyMapsChange = viewModel::onEnabledKeyMapsChange, + onDuplicateClick = viewModel::onDuplicateSelectedKeyMapsClick, + onExportClick = viewModel::onExportSelectedKeyMaps, + onDeleteClick = { showDeleteDialog = true }, + onGroupClick = viewModel::onSelectionGroupClick, + onNewGroupClick = viewModel::onNewGroupClick, + showThisGroup = selectionState.showThisGroup, + onThisGroupClick = viewModel::onMoveToThisGroupClick, + ) + } + }, + ) + } } @Composable diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeScreen.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeScreen.kt index 0381317498..b8ffce2f7a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/HomeScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeScreen.kt @@ -7,14 +7,9 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.Icon @@ -103,10 +98,7 @@ private fun HomeScreen( val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination - Column( - modifier // Only take the horizontal because the status bar is the same color as the app bar - .windowInsetsPadding(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)), - ) { + Column(modifier) { Box(contentAlignment = Alignment.BottomCenter) { NavHost( modifier = Modifier.fillMaxSize(), 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 636a9dea5a..6044fad6cf 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 @@ -13,13 +13,13 @@ import io.github.sds100.keymapper.R import io.github.sds100.keymapper.backup.BackupRestoreMappingsUseCase import io.github.sds100.keymapper.floating.ListFloatingLayoutsUseCase import io.github.sds100.keymapper.floating.ListFloatingLayoutsViewModel -import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCase -import io.github.sds100.keymapper.mappings.keymaps.KeyMapListViewModel -import io.github.sds100.keymapper.mappings.keymaps.ListKeyMapsUseCase -import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardUseCase +import io.github.sds100.keymapper.keymaps.KeyMapListViewModel +import io.github.sds100.keymapper.keymaps.ListKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.sorting.SortKeyMapsUseCase import io.github.sds100.keymapper.system.inputmethod.ShowInputMethodPickerUseCase +import io.github.sds100.keymapper.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.util.ui.DialogResponse import io.github.sds100.keymapper.util.ui.NavigationViewModel import io.github.sds100.keymapper.util.ui.NavigationViewModelImpl @@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -81,6 +80,7 @@ class HomeViewModel( pauseKeyMaps, backupRestore, showInputMethodPickerUseCase, + onboarding, ) } @@ -93,15 +93,13 @@ class HomeViewModel( } init { - - combine( - onboarding.showWhatsNew, - onboarding.showQuickStartGuideHint, - ) { showWhatsNew, showQuickStartGuideHint -> - if (showWhatsNew) { - showWhatsNewDialog() + viewModelScope.launch { + onboarding.showWhatsNew.collect { showWhatsNew -> + if (showWhatsNew) { + showWhatsNewDialog() + } } - }.launchIn(viewModelScope) + } viewModelScope.launch { if (setupGuiKeyboard.isInstalled.first() && !setupGuiKeyboard.isCompatibleVersion.first()) { diff --git a/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt b/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt index 2990b29b39..ea4f429031 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt @@ -25,9 +25,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -100,7 +100,7 @@ import io.github.sds100.keymapper.groups.GroupBreadcrumbRow import io.github.sds100.keymapper.groups.GroupConstraintRow import io.github.sds100.keymapper.groups.GroupListItemModel import io.github.sds100.keymapper.groups.GroupRow -import io.github.sds100.keymapper.mappings.keymaps.KeyMapAppBarState +import io.github.sds100.keymapper.keymaps.KeyMapAppBarState import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.ComposeChipModel @@ -405,6 +405,7 @@ private fun RootGroupAppBar( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChildGroupAppBar( modifier: Modifier = Modifier, @@ -439,7 +440,7 @@ private fun ChildGroupAppBar( Column { Row( Modifier - .statusBarsPadding() + .windowInsetsPadding(TopAppBarDefaults.windowInsets) .fillMaxWidth() .heightIn(min = 48.dp) .padding(vertical = 8.dp) diff --git a/app/src/main/java/io/github/sds100/keymapper/home/ShowHomeScreenAlertsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/home/ShowHomeScreenAlertsUseCase.kt index 84cf3a0fb2..1f21e50589 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/ShowHomeScreenAlertsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/ShowHomeScreenAlertsUseCase.kt @@ -2,12 +2,13 @@ package io.github.sds100.keymapper.home import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.system.accessibility.ServiceAdapter import io.github.sds100.keymapper.system.accessibility.ServiceState import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map /** @@ -52,6 +53,22 @@ class ShowHomeScreenAlertsUseCaseImpl( override fun disableLogging() { preferences.set(Keys.log, false) } + + override val showNotificationPermissionAlert: Flow = + combine( + permissions.isGrantedFlow(Permission.POST_NOTIFICATIONS), + preferences.get(Keys.neverShowNotificationPermissionAlert).map { it ?: false }, + ) { isGranted, neverShow -> + !isGranted && !neverShow + } + + override fun requestNotificationPermission() { + permissions.request(Permission.POST_NOTIFICATIONS) + } + + override fun neverShowNotificationPermissionAlert() { + preferences.set(Keys.neverShowNotificationPermissionAlert, true) + } } interface ShowHomeScreenAlertsUseCase { @@ -68,4 +85,8 @@ interface ShowHomeScreenAlertsUseCase { val isLoggingEnabled: Flow fun disableLogging() + + val showNotificationPermissionAlert: Flow + fun requestNotificationPermission() + fun neverShowNotificationPermissionAlert() } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/ClickType.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ClickType.kt similarity index 73% rename from app/src/main/java/io/github/sds100/keymapper/mappings/ClickType.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ClickType.kt index 4b0a201ca6..3a259bd68e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/ClickType.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ClickType.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings +package io.github.sds100.keymapper.keymaps /** * Created by sds100 on 21/02/2021. diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapFragment.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapFragment.kt similarity index 81% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapFragment.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapFragment.kt index be3ad1e6c7..20e9fc683e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapFragment.kt @@ -1,10 +1,17 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment @@ -65,7 +72,12 @@ class ConfigKeyMapFragment : Fragment() { setContent { KeyMapperTheme { ConfigKeyMapScreen( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ) + .fillMaxSize(), viewModel = viewModel, navigateBack = findNavController()::navigateUp, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapOptionsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapOptionsViewModel.kt similarity index 99% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapOptionsViewModel.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapOptionsViewModel.kt index 2e37ea9002..0a9daa209b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapOptionsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapOptionsViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.graphics.Color import android.graphics.drawable.Drawable diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapScreen.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt similarity index 86% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapScreen.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt index 3814f879f5..f70a0998e8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement @@ -54,11 +54,15 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.canopas.lib.showcase.IntroShowcase import io.github.sds100.keymapper.R import io.github.sds100.keymapper.actions.ActionsScreen import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.constraints.ConstraintsScreen -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerScreen +import io.github.sds100.keymapper.onboarding.OnboardingTapTarget +import io.github.sds100.keymapper.trigger.TriggerScreen +import io.github.sds100.keymapper.util.ui.compose.KeyMapperTapTarget +import io.github.sds100.keymapper.util.ui.compose.keyMapperShowcaseStyle import io.github.sds100.keymapper.util.ui.compose.openUriSafe import kotlinx.coroutines.launch @@ -69,6 +73,9 @@ fun ConfigKeyMapScreen( navigateBack: () -> Unit, ) { val isKeyMapEnabled by viewModel.isEnabled.collectAsStateWithLifecycle() + val showActionTapTarget by viewModel.showActionsTapTarget.collectAsStateWithLifecycle() + val showConstraintTapTarget by viewModel.showConstraintsTapTarget.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } var showBackDialog by rememberSaveable { mutableStateOf(false) } @@ -118,6 +125,11 @@ fun ConfigKeyMapScreen( navigateBack() }, snackbarHostState = snackbarHostState, + showActionTapTarget = showActionTapTarget, + onActionTapTargetCompleted = viewModel::onActionTapTargetCompleted, + showConstraintTapTarget = showConstraintTapTarget, + onConstraintTapTargetCompleted = viewModel::onConstraintTapTargetCompleted, + onSkipTutorialClick = viewModel::onSkipTutorialClick, ) } @@ -134,6 +146,11 @@ private fun ConfigKeyMapScreen( onBackClick: () -> Unit = {}, onDoneClick: () -> Unit = {}, snackbarHostState: SnackbarHostState = SnackbarHostState(), + showActionTapTarget: Boolean = false, + onActionTapTargetCompleted: () -> Unit = {}, + showConstraintTapTarget: Boolean = false, + onConstraintTapTargetCompleted: () -> Unit = {}, + onSkipTutorialClick: () -> Unit = {}, ) { val scope = rememberCoroutineScope() val triggerHelpUrl = stringResource(R.string.url_trigger_guide) @@ -187,22 +204,49 @@ private fun ConfigKeyMapScreen( @Composable fun Tabs() { for ((index, tab) in tabs.withIndex()) { - Tab( - selected = pagerState.targetPage == index, - text = { - Text( - text = getTabTitle(tab), - maxLines = 1, - ) - }, - onClick = { - scope.launch { - pagerState.animateScrollToPage( - tabs.indexOf(tab), + val tapTarget: OnboardingTapTarget? = when { + showActionTapTarget && tab == ConfigKeyMapTab.ACTIONS -> OnboardingTapTarget.CHOOSE_ACTION + showConstraintTapTarget && (tab == ConfigKeyMapTab.CONSTRAINTS || tab == ConfigKeyMapTab.CONSTRAINTS_AND_OPTIONS) -> OnboardingTapTarget.CHOOSE_CONSTRAINT + else -> null + } + + IntroShowcase( + showIntroShowCase = tapTarget != null, + onShowCaseCompleted = if (tapTarget == OnboardingTapTarget.CHOOSE_ACTION) onActionTapTargetCompleted else onConstraintTapTargetCompleted, + dismissOnClickOutside = true, + ) { + var tabModifier: Modifier = Modifier + + if (tapTarget != null) { + tabModifier = tabModifier.introShowCaseTarget( + index = 0, + style = keyMapperShowcaseStyle(), + ) { + KeyMapperTapTarget( + tapTarget = tapTarget, + onSkipClick = onSkipTutorialClick, ) } - }, - ) + } + + Tab( + modifier = tabModifier, + selected = pagerState.targetPage == index, + text = { + Text( + text = getTabTitle(tab), + maxLines = 1, + ) + }, + onClick = { + scope.launch { + pagerState.animateScrollToPage( + tabs.indexOf(tab), + ) + } + }, + ) + } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapUseCase.kt similarity index 96% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapUseCase.kt index 5f69f2bff7..a53db4df8d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapUseCase.kt @@ -1,5 +1,6 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps +import android.database.sqlite.SQLiteConstraintException import io.github.sds100.keymapper.actions.Action import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.actions.RepeatMode @@ -12,24 +13,19 @@ import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository import io.github.sds100.keymapper.data.repositories.FloatingLayoutRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.floating.FloatingButtonEntityMapper -import io.github.sds100.keymapper.mappings.ClickType -import io.github.sds100.keymapper.mappings.FingerprintGestureType -import io.github.sds100.keymapper.mappings.GetDefaultKeyMapOptionsUseCase -import io.github.sds100.keymapper.mappings.GetDefaultKeyMapOptionsUseCaseImpl -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.FingerprintTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.FloatingButtonKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource -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 import io.github.sds100.keymapper.system.accessibility.ServiceAdapter import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.devices.InputDeviceUtils import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.trigger.AssistantTriggerKey +import io.github.sds100.keymapper.trigger.AssistantTriggerType +import io.github.sds100.keymapper.trigger.FingerprintTriggerKey +import io.github.sds100.keymapper.trigger.FloatingButtonKey +import io.github.sds100.keymapper.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.trigger.Trigger +import io.github.sds100.keymapper.trigger.TriggerKey +import io.github.sds100.keymapper.trigger.TriggerKeyDevice +import io.github.sds100.keymapper.trigger.TriggerMode import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.ServiceEvent import io.github.sds100.keymapper.util.State @@ -367,7 +363,7 @@ class ConfigKeyMapUseCaseController( // 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 } + .mapNotNull { it as? io.github.sds100.keymapper.trigger.KeyCodeTriggerKey } .any { keyToCompare -> keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) } @@ -379,7 +375,7 @@ class ConfigKeyMapUseCaseController( consumeKeyEvent = false } - val triggerKey = KeyCodeTriggerKey( + val triggerKey = io.github.sds100.keymapper.trigger.KeyCodeTriggerKey( keyCode = keyCode, device = device, clickType = clickType, @@ -453,7 +449,10 @@ class ConfigKeyMapUseCaseController( 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, is FingerprintTriggerKey -> 0 - is KeyCodeTriggerKey -> Pair(key.keyCode, key.device) + is io.github.sds100.keymapper.trigger.KeyCodeTriggerKey -> Pair( + key.keyCode, + key.device, + ) is FloatingButtonKey -> key.buttonUid } } @@ -553,7 +552,7 @@ class ConfigKeyMapUseCaseController( override fun setTriggerKeyDevice(keyUid: String, device: TriggerKeyDevice) { editTriggerKey(keyUid) { key -> - if (key is KeyCodeTriggerKey) { + if (key is io.github.sds100.keymapper.trigger.KeyCodeTriggerKey) { key.copy(device = device) } else { key @@ -563,7 +562,7 @@ class ConfigKeyMapUseCaseController( override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) { editTriggerKey(keyUid) { key -> - if (key is KeyCodeTriggerKey) { + if (key is io.github.sds100.keymapper.trigger.KeyCodeTriggerKey) { key.copy(consumeEvent = consumeKeyEvent) } else { key @@ -789,7 +788,7 @@ class ConfigKeyMapUseCaseController( false } else { trigger.keys - .mapNotNull { it as? KeyCodeTriggerKey } + .mapNotNull { it as? io.github.sds100.keymapper.trigger.KeyCodeTriggerKey } .any { InputEventUtils.isDpadKeyCode(it.keyCode) } } @@ -851,7 +850,12 @@ class ConfigKeyMapUseCaseController( val keyMap = keyMap.value.dataOrNull() ?: return if (keyMap.dbId == null) { - keyMapRepository.insert(KeyMapEntityMapper.toEntity(keyMap, 0)) + val entity = KeyMapEntityMapper.toEntity(keyMap, 0) + try { + keyMapRepository.insert(entity) + } catch (e: SQLiteConstraintException) { + keyMapRepository.update(entity) + } } else { keyMapRepository.update(KeyMapEntityMapper.toEntity(keyMap, keyMap.dbId)) } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt similarity index 75% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt index 010ba017b0..f86292155e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.os.Bundle import androidx.lifecycle.ViewModel @@ -8,12 +8,12 @@ import io.github.sds100.keymapper.actions.ConfigActionsViewModel import io.github.sds100.keymapper.actions.CreateActionUseCase import io.github.sds100.keymapper.actions.TestActionUseCase import io.github.sds100.keymapper.constraints.ConfigConstraintsViewModel -import io.github.sds100.keymapper.mappings.FingerprintGesturesSupportedUseCase -import io.github.sds100.keymapper.mappings.keymaps.trigger.ConfigTriggerViewModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.RecordTriggerUseCase -import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardUseCase +import io.github.sds100.keymapper.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.purchasing.PurchasingManager +import io.github.sds100.keymapper.trigger.ConfigTriggerViewModel +import io.github.sds100.keymapper.trigger.RecordTriggerUseCase +import io.github.sds100.keymapper.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.ui.utils.getJsonSerializable import io.github.sds100.keymapper.ui.utils.putJsonSerializable import io.github.sds100.keymapper.util.dataOrNull @@ -22,6 +22,7 @@ import io.github.sds100.keymapper.util.ifIsData import io.github.sds100.keymapper.util.ui.ResourceProvider import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -86,6 +87,24 @@ class ConfigKeyMapViewModel( val isKeyMapEdited: Boolean get() = config.isEdited + val showActionsTapTarget: StateFlow = + combine( + onboarding.showTapTarget(OnboardingTapTarget.CHOOSE_ACTION), + config.keyMap, + ) { showTapTarget, keyMapState -> + // Show the choose action tap target if they have recorded a key. + showTapTarget && keyMapState.dataOrNull()?.trigger?.keys?.isNotEmpty() ?: false + }.stateIn(viewModelScope, SharingStarted.Lazily, false) + + val showConstraintsTapTarget: StateFlow = + combine( + onboarding.showTapTarget(OnboardingTapTarget.CHOOSE_CONSTRAINT), + config.keyMap, + ) { showTapTarget, keyMapState -> + // Show the choose constraint tap target if they have added an action. + showTapTarget && keyMapState.dataOrNull()?.actionList?.isNotEmpty() ?: false + }.stateIn(viewModelScope, SharingStarted.Lazily, false) + fun save() = config.save() fun saveState(outState: Bundle) { @@ -118,6 +137,18 @@ class ConfigKeyMapViewModel( config.setEnabled(enabled) } + fun onActionTapTargetCompleted() { + onboarding.completedTapTarget(OnboardingTapTarget.CHOOSE_ACTION) + } + + fun onConstraintTapTargetCompleted() { + onboarding.completedTapTarget(OnboardingTapTarget.CHOOSE_CONSTRAINT) + } + + fun onSkipTutorialClick() { + onboarding.skipTapTargetOnboarding() + } + class Factory( private val config: ConfigKeyMapUseCase, private val testAction: TestActionUseCase, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutActivity.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutActivity.kt similarity index 98% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutActivity.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutActivity.kt index 14a3006dad..48d0e35783 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutActivity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutActivity.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.os.Bundle import androidx.activity.SystemBarStyle diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutScreen.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutScreen.kt similarity index 98% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutScreen.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutScreen.kt index ae124afada..2857e9e4bb 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent @@ -44,8 +44,8 @@ import io.github.sds100.keymapper.constraints.ConstraintMode import io.github.sds100.keymapper.groups.GroupBreadcrumbRow import io.github.sds100.keymapper.groups.GroupListItemModel import io.github.sds100.keymapper.groups.GroupRow -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapListItemModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError +import io.github.sds100.keymapper.trigger.KeyMapListItemModel +import io.github.sds100.keymapper.trigger.TriggerError import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.drawable diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutUseCase.kt similarity index 98% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutUseCase.kt index e910d77733..6d6fb7e9ed 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.content.Intent import android.graphics.drawable.Drawable diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutViewModel.kt similarity index 97% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutViewModel.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutViewModel.kt index 50cde0b7a6..bba8984ef3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/CreateKeyMapShortcutViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.content.Intent import android.graphics.Color @@ -15,8 +15,8 @@ import io.github.sds100.keymapper.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.constraints.ConstraintMode import io.github.sds100.keymapper.constraints.ConstraintUiHelper import io.github.sds100.keymapper.groups.GroupListItemModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapListItemModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerErrorSnapshot +import io.github.sds100.keymapper.trigger.KeyMapListItemModel +import io.github.sds100.keymapper.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.mapData import io.github.sds100.keymapper.util.ui.ResourceProvider diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/DisplayKeyMapUseCase.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/DisplayKeyMapUseCase.kt index 8e87d9d122..b26f655037 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/DisplayKeyMapUseCase.kt @@ -1,15 +1,12 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.graphics.drawable.Drawable -import android.view.KeyEvent import io.github.sds100.keymapper.actions.DisplayActionUseCase import io.github.sds100.keymapper.actions.GetActionErrorUseCase import io.github.sds100.keymapper.constraints.DisplayConstraintUseCase import io.github.sds100.keymapper.constraints.GetConstraintErrorUseCase import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.purchasing.ProductId import io.github.sds100.keymapper.purchasing.PurchasingManager import io.github.sds100.keymapper.shizuku.ShizukuUtils @@ -19,6 +16,9 @@ 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.system.ringtones.RingtoneAdapter +import io.github.sds100.keymapper.trigger.TriggerError +import io.github.sds100.keymapper.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.State @@ -49,18 +49,12 @@ class DisplayKeyMapUseCaseImpl( private val accessibilityServiceAdapter: ServiceAdapter, private val preferences: PreferenceRepository, private val purchasingManager: PurchasingManager, + private val ringtoneAdapter: RingtoneAdapter, getActionError: GetActionErrorUseCase, getConstraintError: GetConstraintErrorUseCase, ) : DisplayKeyMapUseCase, GetActionErrorUseCase by getActionError, GetConstraintErrorUseCase by getConstraintError { - private companion object { - val keysThatRequireDndAccess = arrayOf( - KeyEvent.KEYCODE_VOLUME_DOWN, - KeyEvent.KEYCODE_VOLUME_UP, - ) - } - private val keyMapperImeHelper = KeyMapperImeHelper(inputMethodAdapter) private val showDpadImeSetupError: Flow = @@ -199,6 +193,10 @@ class DisplayKeyMapUseCaseImpl( override fun neverShowDndTriggerError() { preferenceRepository.set(Keys.neverShowDndAccessError, true) } + + override fun getRingtoneLabel(uri: String): Result { + return ringtoneAdapter.getLabel(uri) + } } interface DisplayKeyMapUseCase : diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/FingerprintGestureType.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/FingerprintGestureType.kt similarity index 69% rename from app/src/main/java/io/github/sds100/keymapper/mappings/FingerprintGestureType.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/FingerprintGestureType.kt index 82a1ef2a1b..b0d65cc8a5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/FingerprintGestureType.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/FingerprintGestureType.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings +package io.github.sds100.keymapper.keymaps enum class FingerprintGestureType { SWIPE_DOWN, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/FingerprintGesturesSupportedUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/FingerprintGesturesSupportedUseCase.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/mappings/FingerprintGesturesSupportedUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/FingerprintGesturesSupportedUseCase.kt index ccd9ad8ae1..3052d0afce 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/FingerprintGesturesSupportedUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/FingerprintGesturesSupportedUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings +package io.github.sds100.keymapper.keymaps import android.os.Build import io.github.sds100.keymapper.data.Keys diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/GetDefaultKeyMapOptionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/GetDefaultKeyMapOptionsUseCase.kt similarity index 98% rename from app/src/main/java/io/github/sds100/keymapper/mappings/GetDefaultKeyMapOptionsUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/GetDefaultKeyMapOptionsUseCase.kt index ee88466da6..db17acfe03 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/GetDefaultKeyMapOptionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/GetDefaultKeyMapOptionsUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMap.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMap.kt similarity index 90% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMap.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMap.kt index a03608bc2b..0e33d254ee 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMap.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMap.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.view.KeyEvent import io.github.sds100.keymapper.actions.Action @@ -10,11 +10,10 @@ import io.github.sds100.keymapper.constraints.ConstraintModeEntityMapper import io.github.sds100.keymapper.constraints.ConstraintState import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout import io.github.sds100.keymapper.data.entities.KeyMapEntity -import io.github.sds100.keymapper.mappings.keymaps.detection.KeyMapController -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 io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKey +import io.github.sds100.keymapper.keymaps.detection.KeyMapController +import io.github.sds100.keymapper.trigger.Trigger +import io.github.sds100.keymapper.trigger.TriggerEntityMapper +import io.github.sds100.keymapper.trigger.TriggerKey import kotlinx.serialization.Serializable import java.util.UUID @@ -72,7 +71,7 @@ fun KeyMap.requiresImeKeyEventForwarding(): Boolean { actionList.any { it.data is ActionData.AnswerCall || it.data is ActionData.EndCall } val hasVolumeKeys = trigger.keys - .mapNotNull { it as? KeyCodeTriggerKey } + .mapNotNull { it as? io.github.sds100.keymapper.trigger.KeyCodeTriggerKey } .any { it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || it.keyCode == KeyEvent.KEYCODE_VOLUME_UP @@ -88,7 +87,7 @@ fun KeyMap.requiresImeKeyEventForwarding(): Boolean { * is incoming. */ fun KeyMap.requiresImeKeyEventForwardingInPhoneCall(triggerKey: TriggerKey): Boolean { - if (triggerKey !is KeyCodeTriggerKey) { + if (triggerKey !is io.github.sds100.keymapper.trigger.KeyCodeTriggerKey) { return false } @@ -96,7 +95,7 @@ fun KeyMap.requiresImeKeyEventForwardingInPhoneCall(triggerKey: TriggerKey): Boo actionList.any { it.data is ActionData.AnswerCall || it.data is ActionData.EndCall } val hasVolumeKeys = trigger.keys - .mapNotNull { it as? KeyCodeTriggerKey } + .mapNotNull { it as? io.github.sds100.keymapper.trigger.KeyCodeTriggerKey } .any { it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || it.keyCode == KeyEvent.KEYCODE_VOLUME_UP diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapAppBarState.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapAppBarState.kt similarity index 96% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapAppBarState.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapAppBarState.kt index 12b3030e65..1a7dd98598 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapAppBarState.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapAppBarState.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.constraints.ConstraintMode import io.github.sds100.keymapper.groups.GroupListItemModel diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapGroup.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapGroup.kt similarity index 82% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapGroup.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapGroup.kt index de2910ec78..32a4a17028 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapGroup.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapGroup.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.groups.Group import io.github.sds100.keymapper.util.State diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListItemCreator.kt similarity index 91% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListItemCreator.kt index b8f4d46016..7e99b73df8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListItemCreator.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowForward @@ -9,21 +9,18 @@ import io.github.sds100.keymapper.actions.ActionUiHelper import io.github.sds100.keymapper.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.constraints.ConstraintState import io.github.sds100.keymapper.constraints.ConstraintUiHelper -import io.github.sds100.keymapper.mappings.ClickType -import io.github.sds100.keymapper.mappings.FingerprintGestureType -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.FingerprintTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.FloatingButtonKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapListItemModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerErrorSnapshot -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyDevice -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerMode import io.github.sds100.keymapper.system.devices.InputDeviceUtils import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.trigger.AssistantTriggerKey +import io.github.sds100.keymapper.trigger.AssistantTriggerType +import io.github.sds100.keymapper.trigger.FingerprintTriggerKey +import io.github.sds100.keymapper.trigger.FloatingButtonKey +import io.github.sds100.keymapper.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.trigger.KeyMapListItemModel +import io.github.sds100.keymapper.trigger.Trigger +import io.github.sds100.keymapper.trigger.TriggerErrorSnapshot +import io.github.sds100.keymapper.trigger.TriggerKeyDevice +import io.github.sds100.keymapper.trigger.TriggerMode import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.isFixable import io.github.sds100.keymapper.util.ui.ResourceProvider @@ -58,7 +55,10 @@ class KeyMapListItemCreator( val triggerKeys = keyMap.trigger.keys.map { key -> when (key) { is AssistantTriggerKey -> assistantTriggerKeyName(key) - is KeyCodeTriggerKey -> keyCodeTriggerKeyName(key, showDeviceDescriptors) + is io.github.sds100.keymapper.trigger.KeyCodeTriggerKey -> keyCodeTriggerKeyName( + key, + showDeviceDescriptors, + ) is FloatingButtonKey -> floatingButtonKeyName(key) is FingerprintTriggerKey -> fingerprintKeyName(key) } @@ -241,7 +241,7 @@ class KeyMapListItemCreator( } private fun keyCodeTriggerKeyName( - key: KeyCodeTriggerKey, + key: io.github.sds100.keymapper.trigger.KeyCodeTriggerKey, showDeviceDescriptors: Boolean, ): String = buildString { when (key.clickType) { diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListScreen.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListScreen.kt similarity index 99% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListScreen.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListScreen.kt index 5cf30faa3c..b36da5e53c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -56,8 +56,8 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.constraints.ConstraintMode -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapListItemModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError +import io.github.sds100.keymapper.trigger.KeyMapListItemModel +import io.github.sds100.keymapper.trigger.TriggerError import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.drawable diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListState.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListState.kt similarity index 51% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListState.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListState.kt index 9f79721767..44f6139227 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListState.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListState.kt @@ -1,9 +1,10 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapListItemModel +import io.github.sds100.keymapper.trigger.KeyMapListItemModel import io.github.sds100.keymapper.util.State data class KeyMapListState( val appBarState: KeyMapAppBarState, val listItems: State>, + val showCreateKeyMapTapTarget: Boolean = false, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListViewModel.kt similarity index 91% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListViewModel.kt index 5d4d4e4b7c..9ce55cb086 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapListViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -17,17 +17,18 @@ import io.github.sds100.keymapper.groups.GroupListItemModel import io.github.sds100.keymapper.home.HomeWarningListItem import io.github.sds100.keymapper.home.SelectedKeyMapsEnabled import io.github.sds100.keymapper.home.ShowHomeScreenAlertsUseCase -import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCase -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapListItemModel -import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardState -import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardUseCase -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerErrorSnapshot +import io.github.sds100.keymapper.onboarding.OnboardingTapTarget +import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.sorting.SortKeyMapsUseCase import io.github.sds100.keymapper.sorting.SortViewModel import io.github.sds100.keymapper.system.accessibility.ServiceState import io.github.sds100.keymapper.system.inputmethod.ShowInputMethodPickerUseCase import io.github.sds100.keymapper.system.permissions.Permission +import io.github.sds100.keymapper.trigger.KeyMapListItemModel +import io.github.sds100.keymapper.trigger.SetupGuiKeyboardState +import io.github.sds100.keymapper.trigger.SetupGuiKeyboardUseCase +import io.github.sds100.keymapper.trigger.TriggerError +import io.github.sds100.keymapper.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.State @@ -84,7 +85,7 @@ class KeyMapListViewModel( private val pauseKeyMaps: PauseKeyMapsUseCase, private val backupRestore: BackupRestoreMappingsUseCase, private val showInputMethodPickerUseCase: ShowInputMethodPickerUseCase, - + private val onboarding: OnboardingUseCase, ) : PopupViewModel by PopupViewModelImpl(), ResourceProvider by resourceProvider, NavigationViewModel by NavigationViewModelImpl() { @@ -94,6 +95,7 @@ class KeyMapListViewModel( const val ID_ACCESSIBILITY_SERVICE_CRASHED_LIST_ITEM = "accessibility_service_crashed" const val ID_BATTERY_OPTIMISATION_LIST_ITEM = "battery_optimised" const val ID_LOGGING_ENABLED_LIST_ITEM = "logging_enabled" + const val ID_NOTIFICATION_PERMISSION_DENIED_LIST_ITEM = "notification_permission_denied" } val sortViewModel = SortViewModel(coroutineScope, sortKeyMaps) @@ -111,6 +113,7 @@ class KeyMapListViewModel( isPaused = false, ), listItems = State.Loading, + showCreateKeyMapTapTarget = false, ) private val _state: MutableStateFlow = MutableStateFlow(initialState) val state = _state.asStateFlow() @@ -152,7 +155,8 @@ class KeyMapListViewModel( showAlertsUseCase.accessibilityServiceState, showAlertsUseCase.hideAlerts, showAlertsUseCase.isLoggingEnabled, - ) { isBatteryOptimised, serviceState, isHidden, isLoggingEnabled -> + showAlertsUseCase.showNotificationPermissionAlert, + ) { isBatteryOptimised, serviceState, isHidden, isLoggingEnabled, showNotificationPermissionAlert -> if (isHidden) { return@combine emptyList() } @@ -187,6 +191,15 @@ class KeyMapListViewModel( ) } // don't show a success message for this + if (showNotificationPermissionAlert) { + add( + HomeWarningListItem( + ID_NOTIFICATION_PERMISSION_DENIED_LIST_ITEM, + getString(R.string.home_error_notification_permission), + ), + ) + } + if (isLoggingEnabled) { add( HomeWarningListItem( @@ -306,20 +319,29 @@ class KeyMapListViewModel( } } + val showCreateKeyMapTapTarget = combine( + onboarding.showTapTarget(OnboardingTapTarget.CREATE_KEY_MAP), + onboarding.showWhatsNew, + ) { showTapTarget, showWhatsNew -> + // Only show the tap target if whats new is not showing. + showTapTarget && !showWhatsNew + } + coroutineScope.launch { combine( listItemStateFlow, appBarStateFlow, - ) { listState, appBarState -> - Pair(listState, appBarState) - }.collectLatest { (listState, appBarState) -> + showCreateKeyMapTapTarget, + ) { listState, appBarState, showCreateKeyMapTapTarget -> + Triple(listState, appBarState, showCreateKeyMapTapTarget) + }.collectLatest { (listState, appBarState, showCreateKeyMapTapTarget) -> listState.ifIsData { list -> if (list.isNotEmpty()) { showFabText = false } } - _state.value = KeyMapListState(appBarState, listState) + _state.value = KeyMapListState(appBarState, listState, showCreateKeyMapTapTarget) } } @@ -672,10 +694,29 @@ class KeyMapListViewModel( ID_BATTERY_OPTIMISATION_LIST_ITEM -> showAlertsUseCase.disableBatteryOptimisation() ID_LOGGING_ENABLED_LIST_ITEM -> showAlertsUseCase.disableLogging() + ID_NOTIFICATION_PERMISSION_DENIED_LIST_ITEM -> showNotificationPermissionAlertDialog() } } } + private suspend fun showNotificationPermissionAlertDialog() { + val dialog = PopupUi.Dialog( + title = getString(R.string.dialog_title_request_notification_permission), + message = getText(R.string.dialog_message_request_notification_permission), + positiveButtonText = getString(R.string.pos_turn_on), + negativeButtonText = getString(R.string.neg_no_thanks), + neutralButtonText = getString(R.string.pos_never_show_again), + ) + + val dialogResponse = showPopup("notification_permission_alert", dialog) + + if (dialogResponse == DialogResponse.POSITIVE) { + showAlertsUseCase.requestNotificationPermission() + } else if (dialogResponse == DialogResponse.NEUTRAL) { + showAlertsUseCase.neverShowNotificationPermissionAlert() + } + } + fun onTogglePausedClick() { coroutineScope.launch { if (pauseKeyMaps.isPaused.first()) { @@ -872,4 +913,12 @@ class KeyMapListViewModel( } } } + + fun onTapTargetsCompleted() { + onboarding.completedTapTarget(OnboardingTapTarget.CREATE_KEY_MAP) + } + + fun onSkipTapTargetClick() { + onboarding.skipTapTargetOnboarding() + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapOptionsScreen.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapOptionsScreen.kt similarity index 99% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapOptionsScreen.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapOptionsScreen.kt index 03216b2bf0..87c6a00c5c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapOptionsScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapOptionsScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.content.ClipData import androidx.compose.animation.AnimatedVisibility diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapRepository.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapRepository.kt similarity index 91% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapRepository.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapRepository.kt index 0d42b9c77c..2c16070364 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapRepository.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/KeyMapRepository.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.data.entities.KeyMapEntity import io.github.sds100.keymapper.util.State @@ -17,6 +17,7 @@ interface KeyMapRepository { suspend fun get(uid: String): KeyMapEntity? fun delete(vararg uid: String) suspend fun deleteAll() + fun count(): Flow fun duplicate(vararg uid: String) fun enableById(vararg uid: String) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ListKeyMapsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ListKeyMapsUseCase.kt similarity index 99% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ListKeyMapsUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ListKeyMapsUseCase.kt index d1b233cd92..8fc9f47b8b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ListKeyMapsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ListKeyMapsUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.database.sqlite.SQLiteConstraintException import io.github.sds100.keymapper.R diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/PauseKeyMapsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/PauseKeyMapsUseCase.kt similarity index 80% rename from app/src/main/java/io/github/sds100/keymapper/mappings/PauseKeyMapsUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/PauseKeyMapsUseCase.kt index 3214f13d19..b5acf2b422 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/PauseKeyMapsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/PauseKeyMapsUseCase.kt @@ -1,8 +1,9 @@ -package io.github.sds100.keymapper.mappings +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.media.MediaAdapter +import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import timber.log.Timber @@ -14,6 +15,7 @@ import timber.log.Timber class PauseKeyMapsUseCaseImpl( private val preferenceRepository: PreferenceRepository, private val mediaAdapter: MediaAdapter, + private val ringtoneAdapter: RingtoneAdapter, ) : PauseKeyMapsUseCase { override val isPaused: Flow = @@ -21,7 +23,8 @@ class PauseKeyMapsUseCaseImpl( override fun pause() { preferenceRepository.set(Keys.mappingsPaused, true) - mediaAdapter.stopMedia() + mediaAdapter.stopFileMedia() + ringtoneAdapter.stopPlaying() Timber.d("Pause mappings") } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ShortcutModel.kt similarity index 76% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutModel.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ShortcutModel.kt index ea54953873..b55b82446d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ShortcutModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ShortcutRow.kt similarity index 85% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/ShortcutRow.kt index d8a147549a..6653732dcd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/ShortcutRow.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement @@ -20,12 +20,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerKeyShortcut +import io.github.sds100.keymapper.trigger.TriggerKeyShortcut import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo @@ -97,6 +98,8 @@ private fun ShortcutButton( text = text, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } @@ -140,3 +143,24 @@ private fun PreviewDrawable() { } } } + +@Preview +@Composable +private fun PreviewMultipleLines() { + val ctx = LocalContext.current + val icon = ctx.drawable(R.mipmap.ic_launcher_round) + + KeyMapperTheme { + Surface { + ShortcutRow( + shortcuts = setOf( + ShortcutModel( + icon = ComposeIconInfo.Drawable(icon), + text = "Line 1\nLine 2\nLine 3", + data = TriggerKeyShortcut.FINGERPRINT_GESTURE, + ), + ), + ) + } + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/SimpleMappingController.kt similarity index 97% rename from app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/SimpleMappingController.kt index dc3d19e77c..7ea2439531 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/SimpleMappingController.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.actions.Action import io.github.sds100.keymapper.actions.PerformActionsUseCase @@ -6,8 +6,7 @@ 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.mappings.keymaps.KeyMap -import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.detection.DetectKeyMapsUseCase import io.github.sds100.keymapper.util.InputEventType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectKeyMapModel.kt similarity index 61% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapModel.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectKeyMapModel.kt index 6242e555bd..7023705558 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectKeyMapModel.kt @@ -1,7 +1,7 @@ -package io.github.sds100.keymapper.mappings.keymaps.detection +package io.github.sds100.keymapper.keymaps.detection import io.github.sds100.keymapper.constraints.ConstraintState -import io.github.sds100.keymapper.mappings.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMap data class DetectKeyMapModel( val keyMap: KeyMap, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectKeyMapsUseCase.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectKeyMapsUseCase.kt index fafed46f55..0e0b453212 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectKeyMapsUseCase.kt @@ -1,7 +1,8 @@ -package io.github.sds100.keymapper.mappings.keymaps.detection +package io.github.sds100.keymapper.keymaps.detection import android.accessibilityservice.AccessibilityService import android.os.SystemClock +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.R import io.github.sds100.keymapper.constraints.ConstraintState @@ -12,10 +13,9 @@ import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.groups.Group import io.github.sds100.keymapper.groups.GroupEntityMapper -import io.github.sds100.keymapper.mappings.keymaps.KeyMap -import io.github.sds100.keymapper.mappings.keymaps.KeyMapEntityMapper -import io.github.sds100.keymapper.mappings.keymaps.KeyMapRepository -import io.github.sds100.keymapper.mappings.keymaps.trigger.FingerprintTriggerKey +import io.github.sds100.keymapper.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMapEntityMapper +import io.github.sds100.keymapper.keymaps.KeyMapRepository import io.github.sds100.keymapper.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.inputevents.InputEventInjector @@ -28,6 +28,7 @@ import io.github.sds100.keymapper.system.popup.PopupMessageAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.vibrator.VibratorAdapter import io.github.sds100.keymapper.system.volume.VolumeAdapter +import io.github.sds100.keymapper.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.dataOrNull @@ -140,7 +141,7 @@ class DetectKeyMapsUseCaseImpl( override val detectScreenOffTriggers: Flow = combine( allKeyMapList, - suAdapter.isGranted, + suAdapter.isRooted, ) { keyMapList, isRootPermissionGranted -> keyMapList.any { it.keyMap.trigger.screenOffTrigger } && isRootPermissionGranted }.flowOn(Dispatchers.Default) @@ -193,6 +194,7 @@ class DetectKeyMapsUseCaseImpl( deviceId: Int, inputEventType: InputEventType, scanCode: Int, + source: Int, ) { val model = InputKeyModel( keyCode, @@ -200,6 +202,7 @@ class DetectKeyMapsUseCaseImpl( metaState, deviceId, scanCode, + source = source, ) if (permissionAdapter.isGranted(Permission.SHIZUKU)) { @@ -258,6 +261,7 @@ interface DetectKeyMapsUseCase { deviceId: Int = 0, inputEventType: InputEventType = InputEventType.DOWN_UP, scanCode: Int = 0, + source: Int = InputDevice.SOURCE_UNKNOWN, ) val isScreenOn: Flow diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectScreenOffKeyEventsController.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectScreenOffKeyEventsController.kt index beb9f826eb..a492fe2ec7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DetectScreenOffKeyEventsController.kt @@ -1,5 +1,6 @@ -package io.github.sds100.keymapper.mappings.keymaps.detection +package io.github.sds100.keymapper.keymaps.detection +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.devices.InputDeviceInfo @@ -90,6 +91,7 @@ class DetectScreenOffKeyEventsController( scanCode = 0, metaState = 0, repeatCount = 0, + source = InputDevice.SOURCE_UNKNOWN, ), ) } @@ -103,6 +105,7 @@ class DetectScreenOffKeyEventsController( scanCode = 0, metaState = 0, repeatCount = 0, + source = InputDevice.SOURCE_UNKNOWN, ), ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DpadMotionEventTracker.kt similarity index 96% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DpadMotionEventTracker.kt index 202ba121ad..adc5cc98a3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/DpadMotionEventTracker.kt @@ -1,5 +1,6 @@ -package io.github.sds100.keymapper.mappings.keymaps.detection +package io.github.sds100.keymapper.keymaps.detection +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.system.inputevents.InputEventUtils @@ -107,6 +108,7 @@ class DpadMotionEventTracker { scanCode = 0, device = event.device, repeatCount = 0, + source = InputDevice.SOURCE_DPAD, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/ParallelTriggerActionPerformer.kt similarity index 99% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/detection/ParallelTriggerActionPerformer.kt index 1a8a3ef36d..0050a51d0d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/ParallelTriggerActionPerformer.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.detection +package io.github.sds100.keymapper.keymaps.detection import io.github.sds100.keymapper.actions.Action import io.github.sds100.keymapper.actions.ActionData diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/SequenceTriggerActionPerformer.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/SequenceTriggerActionPerformer.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/SequenceTriggerActionPerformer.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/detection/SequenceTriggerActionPerformer.kt index a5ef9e2b0d..aa04286de1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/SequenceTriggerActionPerformer.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/SequenceTriggerActionPerformer.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.detection +package io.github.sds100.keymapper.keymaps.detection import io.github.sds100.keymapper.actions.Action import io.github.sds100.keymapper.actions.PerformActionsUseCase diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt similarity index 88% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt rename to app/src/main/java/io/github/sds100/keymapper/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt index 089a482d2a..e780051f67 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt @@ -1,9 +1,9 @@ -package io.github.sds100.keymapper.mappings.keymaps.detection +package io.github.sds100.keymapper.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.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.SimpleMappingController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/app/src/main/java/io/github/sds100/keymapper/logging/LogFragment.kt b/app/src/main/java/io/github/sds100/keymapper/logging/LogFragment.kt index 6e1effbd57..50760f51d0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/logging/LogFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/logging/LogFragment.kt @@ -44,7 +44,7 @@ class LogFragment : SimpleRecyclerViewFragment() { private val recyclerViewController by lazy { RecyclerViewController() } private val saveLogToFileLauncher = - registerForActivityResult(CreateDocument(FileUtils.MIME_TYPE_ZIP)) { + registerForActivityResult(CreateDocument(FileUtils.MIME_TYPE_TEXT)) { it ?: return@registerForActivityResult viewModel.onPickFileToSaveTo(it.toString()) 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 deleted file mode 100644 index 5f97f455ad..0000000000 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerButtonRow.kt +++ /dev/null @@ -1,154 +0,0 @@ -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.width -import androidx.compose.material3.Badge -import androidx.compose.material3.ButtonDefaults -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.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -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 - -@Composable -fun RecordTriggerButtonRow( - modifier: Modifier = Modifier, - onRecordTriggerClick: () -> Unit = {}, - recordTriggerState: RecordTriggerState, - onAdvancedTriggersClick: () -> Unit = {}, - showNewBadge: Boolean, -) { - 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.CountingDown, - onClick = onAdvancedTriggersClick, - showNewBadge = showNewBadge, - ) - } -} - -@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) - - else -> - stringResource(R.string.button_record_trigger) - } - - FilledTonalButton( - modifier = modifier, - onClick = onClick, - colors = colors, - ) { - Text( - text = text, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } -} - -@Composable -private fun AdvancedTriggersButton( - modifier: Modifier, - isEnabled: Boolean, - showNewBadge: Boolean, - onClick: () -> Unit, -) { - Box(modifier = modifier) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp), - enabled = isEnabled, - onClick = onClick, - ) { - Text( - text = stringResource(R.string.button_advanced_triggers), - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - - if (showNewBadge) { - 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), - showNewBadge = true, - ) - } - } -} - -@Preview(widthDp = 400) -@Composable -private fun PreviewStopped() { - KeyMapperTheme { - Surface { - RecordTriggerButtonRow( - modifier = Modifier.fillMaxWidth(), - recordTriggerState = RecordTriggerState.Idle, - showNewBadge = false, - ) - } - } -} diff --git a/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingTapTarget.kt b/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingTapTarget.kt new file mode 100644 index 0000000000..b24f1b41db --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/onboarding/OnboardingTapTarget.kt @@ -0,0 +1,34 @@ +package io.github.sds100.keymapper.onboarding + +import androidx.annotation.StringRes +import io.github.sds100.keymapper.R + +enum class OnboardingTapTarget( + @StringRes val titleRes: Int, + @StringRes val messageRes: Int, +) { + CREATE_KEY_MAP( + titleRes = R.string.tap_target_create_key_map_title, + messageRes = R.string.tap_target_create_key_map_message, + ), + + RECORD_TRIGGER( + titleRes = R.string.tap_target_record_trigger_title, + messageRes = R.string.tap_target_record_trigger_message, + ), + + ADVANCED_TRIGGERS( + titleRes = R.string.tap_target_advanced_triggers_title, + messageRes = R.string.tap_target_advanced_triggers_message, + ), + + CHOOSE_ACTION( + titleRes = R.string.tap_target_choose_action_title, + messageRes = R.string.tap_target_choose_action_message, + ), + + CHOOSE_CONSTRAINT( + titleRes = R.string.tap_target_choose_constraint_title, + messageRes = R.string.tap_target_choose_constraint_message, + ), +} 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 73b90c0118..3bd5309ca0 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,11 +1,16 @@ package io.github.sds100.keymapper.onboarding +import androidx.datastore.preferences.core.Preferences import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.actions.canUseImeToPerform import io.github.sds100.keymapper.actions.canUseShizukuToPerform import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.entities.KeyMapEntity import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.keymaps.KeyMapRepository +import io.github.sds100.keymapper.purchasing.ProductId +import io.github.sds100.keymapper.purchasing.PurchasingManager import io.github.sds100.keymapper.shizuku.ShizukuAdapter import io.github.sds100.keymapper.shizuku.ShizukuUtils import io.github.sds100.keymapper.system.apps.PackageManagerAdapter @@ -15,9 +20,13 @@ import io.github.sds100.keymapper.system.leanback.LeanbackAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.util.PrefDelegate +import io.github.sds100.keymapper.util.Result +import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.VersionHelper +import io.github.sds100.keymapper.util.handle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -31,6 +40,8 @@ class OnboardingUseCaseImpl( private val shizukuAdapter: ShizukuAdapter, private val permissionAdapter: PermissionAdapter, private val packageManagerAdapter: PackageManagerAdapter, + private val purchasingManager: PurchasingManager, + private val keyMapRepository: KeyMapRepository, ) : PreferenceRepository by preferences, OnboardingUseCase { @@ -101,18 +112,6 @@ class OnboardingUseCaseImpl( set(Keys.lastInstalledVersionCodeBackground, Constants.VERSION_CODE) } - override val showQuickStartGuideHint: Flow = get(Keys.shownQuickStartGuideHint).map { - if (it == null) { - true - } else { - !it - } - } - - override fun shownQuickStartGuideHint() { - preferences.set(Keys.shownQuickStartGuideHint, true) - } - override fun isTvDevice(): Boolean = leanbackAdapter.isTvDevice() override val promptForShizukuPermission: Flow = combine( @@ -149,6 +148,86 @@ class OnboardingUseCaseImpl( override fun viewedAdvancedTriggers() { set(Keys.viewedAdvancedTriggers, true) } + + override fun showTapTarget(tapTarget: OnboardingTapTarget): Flow { + val shownKey = getTapTargetKey(tapTarget) + + if (tapTarget == OnboardingTapTarget.ADVANCED_TRIGGERS) { + return combine( + preferences.get(shownKey).map { it ?: false }, + purchasingManager.purchases.filterIsInstance>>>(), + keyMapRepository.keyMapList.filterIsInstance>>(), + ) { isShown, purchases, keyMapList -> + // Only show the tap target for advanced triggers if it has not already been shown + // and the user has not made any purchases. Also, the user must have saved a working key map + // as a heuristic for they actually interacted with Key Mapper a bit before + // pushing them to paid features. + !isShown && + keyMapList.data.any { it.trigger.keys.isNotEmpty() && it.actionList.isNotEmpty() } && + purchases.data.handle( + onSuccess = { it.isEmpty() }, + onError = { false }, + ) + } + } else { + return combine( + preferences.get(shownKey).map { it ?: false }, + preferences.get(Keys.skipTapTargetTutorial).map { it ?: false }, + keyMapRepository.keyMapList.filterIsInstance>>(), + ) { isShown, skipTapTarget, keyMapList -> + showTutorialTapTarget(tapTarget, isShown, skipTapTarget, keyMapList.data) + } + } + } + + override fun completedTapTarget(tapTarget: OnboardingTapTarget) { + val key = getTapTargetKey(tapTarget) + preferences.set(key, true) + } + + private fun getTapTargetKey(tapTarget: OnboardingTapTarget): Preferences.Key { + val key = when (tapTarget) { + OnboardingTapTarget.CREATE_KEY_MAP -> Keys.shownTapTargetCreateKeyMap + OnboardingTapTarget.RECORD_TRIGGER -> Keys.shownTapTargetRecordTrigger + OnboardingTapTarget.ADVANCED_TRIGGERS -> Keys.shownTapTargetAdvancedTriggers + OnboardingTapTarget.CHOOSE_ACTION -> Keys.shownTapTargetChooseAction + OnboardingTapTarget.CHOOSE_CONSTRAINT -> Keys.shownTapTargetChooseConstraint + } + return key + } + + /** + * Whether to show a tutorial tap target. This will try to determine whether the user + * has interacted with each feature before by checking the key maps they've created (if any). + * E.g if they have no key maps with actions then show a tap target highlighting the action tab + * when they create a key map. + */ + private fun showTutorialTapTarget( + tapTarget: OnboardingTapTarget, + isShown: Boolean, + skipTutorial: Boolean, + keyMapList: List, + ): Boolean { + if (isShown) { + return false + } + + if (skipTutorial) { + return false + } + + return when (tapTarget) { + OnboardingTapTarget.CREATE_KEY_MAP -> keyMapList.isEmpty() + OnboardingTapTarget.RECORD_TRIGGER -> keyMapList.all { it.trigger.keys.isEmpty() } + OnboardingTapTarget.CHOOSE_ACTION -> keyMapList.all { it.actionList.isEmpty() } + OnboardingTapTarget.CHOOSE_CONSTRAINT -> keyMapList.all { it.constraintList.isEmpty() } + else -> throw IllegalArgumentException("This is not a tutorial tap target: $tapTarget") + } + } + + override fun skipTapTargetOnboarding() { + preferences.set(Keys.skipTapTargetTutorial, true) + } } interface OnboardingUseCase { @@ -181,9 +260,6 @@ interface OnboardingUseCase { fun showedWhatsNew() fun getWhatsNewText(): String - val showQuickStartGuideHint: Flow - fun shownQuickStartGuideHint() - val promptForShizukuPermission: Flow val showShizukuAppIntroSlide: Boolean @@ -193,4 +269,8 @@ interface OnboardingUseCase { val hasViewedAdvancedTriggers: Flow fun viewedAdvancedTriggers() + + fun showTapTarget(tapTarget: OnboardingTapTarget): Flow + fun completedTapTarget(tapTarget: OnboardingTapTarget) + fun skipTapTargetOnboarding() } diff --git a/app/src/main/java/io/github/sds100/keymapper/promode/ProModeFragment.kt b/app/src/main/java/io/github/sds100/keymapper/promode/ProModeFragment.kt new file mode 100644 index 0000000000..cb31934545 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/promode/ProModeFragment.kt @@ -0,0 +1,72 @@ +package io.github.sds100.keymapper.promode + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.findNavController +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.databinding.FragmentComposeBinding +import io.github.sds100.keymapper.util.Inject +import io.github.sds100.keymapper.util.ui.setupNavigation +import io.github.sds100.keymapper.util.ui.showPopups + +class ProModeFragment : Fragment() { + + private val viewModel by viewModels { + Inject.proModeViewModel(requireContext()) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + viewModel.setupNavigation(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + FragmentComposeBinding.inflate(inflater, container, false).apply { + composeView.apply { + // Dispose of the Composition when the view's LifecycleOwner + // is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + KeyMapperTheme { + ProModeScreen( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ), + viewModel = viewModel, + onNavigateBack = findNavController()::navigateUp, + ) + } + } + } + return this.root + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.showPopups(this, view) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/promode/ProModeScreen.kt b/app/src/main/java/io/github/sds100/keymapper/promode/ProModeScreen.kt new file mode 100644 index 0000000000..4628e68aa7 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/promode/ProModeScreen.kt @@ -0,0 +1,344 @@ +package io.github.sds100.keymapper.promode + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Android +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Checklist +import androidx.compose.material.icons.rounded.Numbers +import androidx.compose.material.icons.rounded.WarningAmber +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.compose.LocalCustomColorsPalette +import io.github.sds100.keymapper.util.ui.compose.OptionsHeaderRow + +@Composable +fun ProModeScreen( + modifier: Modifier = Modifier, + viewModel: ProModeViewModel, + onNavigateBack: () -> Unit, +) { + val proModeWarningState by viewModel.proModeWarningState.collectAsStateWithLifecycle() + + ProModeScreen(modifier = modifier, onBackClick = onNavigateBack) { + Content( + proModeWarningState = proModeWarningState, + onWarningButtonClick = viewModel::onWarningButtonClick, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ProModeScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + content: @Composable () -> Unit, +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.pro_mode_app_bar_title)) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.action_go_back), + ) + } + }, + ) + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + content() + } + } +} + +@Composable +private fun Content( + modifier: Modifier = Modifier, + proModeWarningState: ProModeWarningState, + onWarningButtonClick: () -> Unit = {}, +) { + Column(modifier = modifier.verticalScroll(rememberScrollState())) { + WarningCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + state = proModeWarningState, + onButtonClick = onWarningButtonClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (proModeWarningState is ProModeWarningState.Understood) { + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.Checklist, + text = stringResource(R.string.pro_mode_set_up_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SetupCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + color = LocalCustomColorsPalette.current.magiskTeal, + icon = Icons.Rounded.Numbers, + title = stringResource(R.string.pro_mode_root_detected_title), + content = { + Text( + text = stringResource(R.string.pro_mode_root_detected_text), + style = MaterialTheme.typography.bodyMedium, + ) + }, + buttonText = stringResource(R.string.pro_mode_root_detected_button), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SetupCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + color = LocalCustomColorsPalette.current.shizukuBlue, + icon = Icons.Rounded.Android, + title = stringResource(R.string.pro_mode_shizuku_detected_title), + content = { + Text( + text = stringResource(R.string.pro_mode_shizuku_detected_text), + style = MaterialTheme.typography.bodyMedium, + ) + }, + buttonText = stringResource(R.string.pro_mode_shizuku_detected_button), + ) + } else { + Text( + modifier = Modifier.padding(horizontal = 32.dp), + text = stringResource(R.string.pro_mode_settings_unavailable_text), + textAlign = TextAlign.Center, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Composable +private fun WarningCard( + modifier: Modifier = Modifier, + state: ProModeWarningState, + onButtonClick: () -> Unit = {}, +) { + OutlinedCard( + modifier = modifier, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), + ) { + Spacer(modifier = Modifier.height(16.dp)) + Row(modifier = Modifier.padding(horizontal = 16.dp)) { + Icon( + imageVector = Icons.Rounded.WarningAmber, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(R.string.pro_mode_warning_title), + style = MaterialTheme.typography.titleMedium, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.pro_mode_warning_text), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + FilledTonalButton( + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 16.dp), + onClick = onButtonClick, + enabled = state is ProModeWarningState.Idle, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + if (state is ProModeWarningState.Understood) { + Icon(imageVector = Icons.Rounded.Check, contentDescription = null) + + Spacer(modifier = Modifier.width(8.dp)) + } + + val text = when (state) { + is ProModeWarningState.CountingDown -> stringResource( + R.string.pro_mode_warning_understand_button_countdown, + state.seconds, + ) + + ProModeWarningState.Idle -> stringResource(R.string.pro_mode_warning_understand_button_not_completed) + ProModeWarningState.Understood -> stringResource(R.string.pro_mode_warning_understand_button_completed) + } + + Text(text) + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun SetupCard( + modifier: Modifier = Modifier, + color: Color, + icon: ImageVector, + title: String, + content: @Composable () -> Unit, + buttonText: String, + onButtonClick: () -> Unit = {}, +) { + OutlinedCard(modifier = modifier) { + Spacer(modifier = Modifier.height(16.dp)) + Row(modifier = Modifier.padding(horizontal = 16.dp)) { + Icon( + imageVector = icon, + contentDescription = null, + tint = color, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + content() + } + + Spacer(modifier = Modifier.height(8.dp)) + + FilledTonalButton( + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 16.dp), + onClick = onButtonClick, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = color, + contentColor = LocalCustomColorsPalette.current.contentColorFor(color), + ), + ) { + Text(buttonText) + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + ProModeScreen { + Content( + proModeWarningState = ProModeWarningState.Understood, + ) + } + } +} + +@Preview +@Composable +private fun PreviewDark() { + KeyMapperTheme(darkTheme = true) { + ProModeScreen { + Content( + proModeWarningState = ProModeWarningState.Understood, + ) + } + } +} + +@Preview +@Composable +private fun PreviewCountingDown() { + KeyMapperTheme { + ProModeScreen { + Content( + proModeWarningState = ProModeWarningState.CountingDown( + seconds = 5, + ), + ) + } + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/promode/ProModeSetupUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/promode/ProModeSetupUseCase.kt new file mode 100644 index 0000000000..c97d1f9b61 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/promode/ProModeSetupUseCase.kt @@ -0,0 +1,22 @@ +package io.github.sds100.keymapper.promode + +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ProModeSetupUseCaseImpl( + private val preferences: PreferenceRepository, +) : ProModeSetupUseCase { + override val isWarningUnderstood: Flow = + preferences.get(Keys.isProModeWarningUnderstood).map { it ?: false } + + override fun onUnderstoodWarning() { + preferences.set(Keys.isProModeWarningUnderstood, true) + } +} + +interface ProModeSetupUseCase { + val isWarningUnderstood: Flow + fun onUnderstoodWarning() +} diff --git a/app/src/main/java/io/github/sds100/keymapper/promode/ProModeViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/promode/ProModeViewModel.kt new file mode 100644 index 0000000000..d2f6e531f5 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/promode/ProModeViewModel.kt @@ -0,0 +1,79 @@ +package io.github.sds100.keymapper.promode + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import io.github.sds100.keymapper.util.ui.NavigationViewModel +import io.github.sds100.keymapper.util.ui.NavigationViewModelImpl +import io.github.sds100.keymapper.util.ui.PopupViewModel +import io.github.sds100.keymapper.util.ui.PopupViewModelImpl +import io.github.sds100.keymapper.util.ui.ResourceProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn + +class ProModeViewModel( + resourceProvider: ResourceProvider, + private val useCase: ProModeSetupUseCase, +) : ViewModel(), + ResourceProvider by resourceProvider, + PopupViewModel by PopupViewModelImpl(), + NavigationViewModel by NavigationViewModelImpl() { + + companion object { + private const val WARNING_COUNT_DOWN_DURATION = 5 + } + + @OptIn(ExperimentalCoroutinesApi::class) + val proModeWarningState: StateFlow = + useCase.isWarningUnderstood.flatMapLatest { isUnderstood -> + if (isUnderstood) { + flowOf(ProModeWarningState.Understood) + } else { + flow { + repeat(WARNING_COUNT_DOWN_DURATION) { + emit(ProModeWarningState.CountingDown(WARNING_COUNT_DOWN_DURATION - it)) + delay(1000L) + } + + emit(ProModeWarningState.Idle) + } + } + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, + ProModeWarningState.CountingDown( + WARNING_COUNT_DOWN_DURATION, + ), + ) + + fun onWarningButtonClick() { + useCase.onUnderstoodWarning() + } + + @Suppress("UNCHECKED_CAST") + class Factory( + private val resourceProvider: ResourceProvider, + private val useCase: ProModeSetupUseCase, + ) : ViewModelProvider.NewInstanceFactory() { + + override fun create(modelClass: Class): T = ProModeViewModel(resourceProvider, useCase) as T + } +} + +sealed class ProModeWarningState { + data class CountingDown(val seconds: Int) : ProModeWarningState() + data object Idle : ProModeWarningState() + data object Understood : ProModeWarningState() +} + +data class ProModeSetupState( + val isRootDetected: Boolean, + val isShizukuDetected: Boolean, + +) diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt index fbc4707022..bc94307a06 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt @@ -41,7 +41,7 @@ class ConfigSettingsUseCaseImpl( private val imeHelper by lazy { KeyMapperImeHelper(inputMethodAdapter) } - override val isRootGranted: Flow = suAdapter.isGranted + override val isRootGranted: Flow = suAdapter.isRooted override val isWriteSecureSettingsGranted: Flow = channelFlow { send(permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)) @@ -157,6 +157,10 @@ class ConfigSettingsUseCaseImpl( permissionAdapter.request(Permission.POST_NOTIFICATIONS) } + override fun requestRootPermission() { + suAdapter.requestPermission() + } + override fun isNotificationsPermissionGranted(): Boolean = permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS) override fun getSoundFiles(): List = soundsManager.soundFiles.value @@ -179,37 +183,38 @@ interface ConfigSettingsUseCase { fun setAutomaticBackupLocation(uri: String) fun disableAutomaticBackup() val isRootGranted: Flow - val isWriteSecureSettingsGranted: Flow + fun requestRootPermission() + val isWriteSecureSettingsGranted: Flow val isShizukuInstalled: Flow val isShizukuStarted: Flow val isShizukuPermissionGranted: Flow fun downloadShizuku() - fun openShizukuApp() + fun openShizukuApp() val rerouteKeyEvents: Flow val isCompatibleImeChosen: Flow val isCompatibleImeEnabled: Flow suspend fun enableCompatibleIme() suspend fun chooseCompatibleIme(): Result - suspend fun showImePicker(): Result<*> + suspend fun showImePicker(): Result<*> val defaultLongPressDelay: Flow val defaultDoublePressDelay: Flow val defaultRepeatDelay: Flow val defaultSequenceTriggerTimeout: Flow val defaultVibrateDuration: Flow - val defaultRepeatRate: Flow + val defaultRepeatRate: Flow fun getSoundFiles(): List fun deleteSoundFiles(uid: List) fun resetDefaultMappingOptions() fun requestWriteSecureSettingsPermission() fun requestNotificationsPermission() fun isNotificationsPermissionGranted(): Boolean + fun requestShizukuPermission() val connectedInputDevices: StateFlow>> - fun resetAllSettings() } diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt b/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt index 5c887d835e..1cc746b57d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt @@ -27,6 +27,7 @@ import io.github.sds100.keymapper.util.firstBlocking 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.setupNavigation import io.github.sds100.keymapper.util.viewLifecycleScope import kotlinx.coroutines.flow.collectLatest import splitties.alertdialog.appcompat.alertDialog @@ -60,6 +61,12 @@ class MainSettingsFragment : BaseSettingsFragment() { requireContext().contentResolver.takePersistableUriPermission(it, takeFlags) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + viewModel.setupNavigation(this) + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper addPreferencesFromResource(R.xml.preferences_empty) @@ -127,6 +134,21 @@ class MainSettingsFragment : BaseSettingsFragment() { } private fun populatePreferenceScreen() = preferenceScreen.apply { + // Pro mode + Preference(requireContext()).apply { + isSingleLineTitle = false + + setTitle(R.string.title_pref_pro_mode) + setSummary(R.string.summary_pref_pro_mode) + + setOnPreferenceClickListener { + viewModel.onProModeClick() + true + } + + addPreference(this) + } + // dark theme DropDownPreference(requireContext()).apply { key = Keys.darkTheme.name @@ -490,14 +512,16 @@ class MainSettingsFragment : BaseSettingsFragment() { } // root permission switch - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.hasRootPermission.name - setDefaultValue(false) - + Preference(requireContext()).apply { isSingleLineTitle = false setTitle(R.string.title_pref_root_permission) setSummary(R.string.summary_pref_root_permission) + setOnPreferenceClickListener { + viewModel.onRequestRootClick() + true + } + addPreference(this) } diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt index 6c96e76939..abb848bf48 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt @@ -13,10 +13,14 @@ import io.github.sds100.keymapper.util.onSuccess import io.github.sds100.keymapper.util.otherwise import io.github.sds100.keymapper.util.ui.DialogResponse import io.github.sds100.keymapper.util.ui.MultiChoiceItem +import io.github.sds100.keymapper.util.ui.NavDestination +import io.github.sds100.keymapper.util.ui.NavigationViewModel +import io.github.sds100.keymapper.util.ui.NavigationViewModelImpl import io.github.sds100.keymapper.util.ui.PopupUi import io.github.sds100.keymapper.util.ui.PopupViewModel import io.github.sds100.keymapper.util.ui.PopupViewModelImpl import io.github.sds100.keymapper.util.ui.ResourceProvider +import io.github.sds100.keymapper.util.ui.navigate import io.github.sds100.keymapper.util.ui.showPopup import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -32,6 +36,7 @@ class SettingsViewModel( private val useCase: ConfigSettingsUseCase, resourceProvider: ResourceProvider, ) : ViewModel(), + NavigationViewModel by NavigationViewModelImpl(), PopupViewModel by PopupViewModelImpl(), ResourceProvider by resourceProvider { val sharedPrefsDataStoreWrapper = SharedPrefsDataStoreWrapper(useCase) @@ -211,6 +216,16 @@ class SettingsViewModel( } } + fun onRequestRootClick() { + useCase.requestRootPermission() + } + + fun onProModeClick() { + viewModelScope.launch { + navigate("pro_mode_settings", NavDestination.ProMode) + } + } + @Suppress("UNCHECKED_CAST") class Factory( private val configSettingsUseCase: ConfigSettingsUseCase, diff --git a/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt index 9608b58f1c..2b812d21c0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt @@ -8,7 +8,6 @@ import android.view.KeyEvent import io.github.sds100.keymapper.system.inputevents.InputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.util.InputEventType -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import rikka.shizuku.ShizukuBinderWrapper @@ -16,7 +15,7 @@ import rikka.shizuku.SystemServiceHelper import timber.log.Timber @SuppressLint("PrivateApi") -class ShizukuInputEventInjector(private val coroutineScope: CoroutineScope) : InputEventInjector { +class ShizukuInputEventInjector : InputEventInjector { companion object { // private const val INJECT_INPUT_EVENT_MODE_ASYNC = 0 @@ -40,16 +39,7 @@ class ShizukuInputEventInjector(private val coroutineScope: CoroutineScope) : In val eventTime = SystemClock.uptimeMillis() - val keyEvent = KeyEvent( - eventTime, - eventTime, - action, - model.keyCode, - model.repeat, - model.metaState, - model.deviceId, - model.scanCode, - ) + val keyEvent = createInjectedKeyEvent(eventTime, action, model) withContext(Dispatchers.IO) { // MUST wait for the application to finish processing the event before sending the next one. diff --git a/app/src/main/java/io/github/sds100/keymapper/sorting/SortBottomSheetContent.kt b/app/src/main/java/io/github/sds100/keymapper/sorting/SortBottomSheetContent.kt index fb80600a7d..fbfedd964e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/sorting/SortBottomSheetContent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/sorting/SortBottomSheetContent.kt @@ -70,8 +70,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.compose.draggable.DraggableItem -import io.github.sds100.keymapper.compose.draggable.rememberDragDropState +import io.github.sds100.keymapper.util.ui.compose.DraggableItem +import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/app/src/main/java/io/github/sds100/keymapper/sorting/SortKeyMapsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/sorting/SortKeyMapsUseCase.kt index c263292258..03b9ca6835 100644 --- a/app/src/main/java/io/github/sds100/keymapper/sorting/SortKeyMapsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/sorting/SortKeyMapsUseCase.kt @@ -2,15 +2,14 @@ package io.github.sds100.keymapper.sorting import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.mappings.keymaps.DisplayKeyMapUseCase -import io.github.sds100.keymapper.mappings.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.DisplayKeyMapUseCase +import io.github.sds100.keymapper.keymaps.KeyMap import io.github.sds100.keymapper.sorting.comparators.KeyMapActionsComparator import io.github.sds100.keymapper.sorting.comparators.KeyMapConstraintsComparator import io.github.sds100.keymapper.sorting.comparators.KeyMapOptionsComparator import io.github.sds100.keymapper.sorting.comparators.KeyMapTriggerComparator import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json class SortKeyMapsUseCaseImpl( diff --git a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapActionsComparator.kt b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapActionsComparator.kt index 1feb8fc728..479b5a3922 100644 --- a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapActionsComparator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapActionsComparator.kt @@ -2,7 +2,7 @@ package io.github.sds100.keymapper.sorting.comparators import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.actions.DisplayActionUseCase -import io.github.sds100.keymapper.mappings.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMap import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.Success import io.github.sds100.keymapper.util.valueOrNull @@ -68,7 +68,8 @@ class KeyMapActionsComparator( is ActionData.App -> displayActions.getAppName(action.packageName) is ActionData.AppShortcut -> Success(action.shortcutTitle) is ActionData.InputKeyEvent -> Success(action.keyCode.toString()) - is ActionData.Sound -> Success(action.soundDescription) + is ActionData.Sound.SoundFile -> Success(action.soundDescription) + is ActionData.Sound.Ringtone -> Success(action.uri) is ActionData.Volume.Stream -> Success(action.volumeStream.toString()) is ActionData.Volume.SetRingerMode -> Success(action.ringerMode.toString()) is ActionData.Flashlight -> Success(action.lens.toString()) diff --git a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapConstraintsComparator.kt b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapConstraintsComparator.kt index 7ba746f11e..036e4ec9af 100644 --- a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapConstraintsComparator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapConstraintsComparator.kt @@ -2,7 +2,7 @@ package io.github.sds100.keymapper.sorting.comparators import io.github.sds100.keymapper.constraints.Constraint import io.github.sds100.keymapper.constraints.DisplayConstraintUseCase -import io.github.sds100.keymapper.mappings.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMap import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.Success import io.github.sds100.keymapper.util.then diff --git a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapOptionsComparator.kt b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapOptionsComparator.kt index a891a23393..27cf6c9ed4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapOptionsComparator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapOptionsComparator.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.sorting.comparators -import io.github.sds100.keymapper.mappings.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMap class KeyMapOptionsComparator( /** diff --git a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapTriggerComparator.kt b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapTriggerComparator.kt index b5425499c7..47ef0131aa 100644 --- a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapTriggerComparator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapTriggerComparator.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.sorting.comparators -import io.github.sds100.keymapper.mappings.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.KeyMap class KeyMapTriggerComparator( /** diff --git a/app/src/main/java/io/github/sds100/keymapper/system/Shell.kt b/app/src/main/java/io/github/sds100/keymapper/system/SimpleShell.kt similarity index 97% rename from app/src/main/java/io/github/sds100/keymapper/system/Shell.kt rename to app/src/main/java/io/github/sds100/keymapper/system/SimpleShell.kt index 10b043e54d..0d0fcd65bf 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/Shell.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/SimpleShell.kt @@ -10,7 +10,7 @@ import java.io.InputStream /** * Created by sds100 on 05/11/2018. */ -object Shell : ShellAdapter { +object SimpleShell : ShellAdapter { /** * @return whether the command was executed successfully */ diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt index b0e1250a56..becc2fcf78 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.system.accessibility -import android.graphics.Rect +import android.accessibilityservice.AccessibilityService import android.os.Build import android.os.CountDownTimer import android.view.accessibility.AccessibilityEvent @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.update class AccessibilityNodeRecorder( private val nodeRepository: AccessibilityNodeRepository, + private val service: AccessibilityService, ) { companion object { private const val RECORD_DURATION = 60000L @@ -60,51 +61,54 @@ class AccessibilityNodeRecorder( return } - val source = event.source ?: return - val sourceBounds = Rect() - source.getBoundsInScreen(sourceBounds) - - val root: AccessibilityNodeInfo = source.window.root ?: return - - // This searches for all nodes that are within the bounds of the source of the - // AccessibilityEvent because the source is not necessarily the element - // the user wants to tap. - val entities = getNodesInBounds(root, sourceBounds).toTypedArray() - nodeRepository.insert(*entities) + if (event.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED || + event.eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED + ) { + val source = event.source ?: return + + buildNodeEntity(source, interacted = true)?.also { nodeRepository.insert(it) } + } else if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { + // Only dump the whole window when a window is added because there can be + // many windows changed events sent in rapid succession. + val windowRoot: AccessibilityNodeInfo = service.rootInActiveWindow ?: return + + // This searches for all nodes that are within the bounds of the source of the + // AccessibilityEvent because the source is not necessarily the element + // the user wants to tap. + val entities = getNodesRecursively(windowRoot).toTypedArray() + nodeRepository.insert(*entities) + } } - /** - * Get all the nodes that are within the given bounds. - */ - private fun getNodesInBounds( + private fun getNodesRecursively( node: AccessibilityNodeInfo, - bounds: Rect, ): Set { val set = mutableSetOf() - val nodeBounds = Rect() - node.getBoundsInScreen(nodeBounds) - - if (bounds.contains(nodeBounds)) { - val entity = buildNodeEntity(node) + val entity = buildNodeEntity(node, interacted = false) - if (entity != null) { - set.add(entity) - } + if (entity != null) { + set.add(entity) } if (node.childCount > 0) { for (i in 0 until node.childCount) { val child = node.getChild(i) ?: continue - set.addAll(getNodesInBounds(child, bounds)) + set.addAll(getNodesRecursively(child)) } } return set } - private fun buildNodeEntity(source: AccessibilityNodeInfo): AccessibilityNodeEntity? { + /** + * @param interacted Whether the user interacted with this node. + */ + private fun buildNodeEntity( + source: AccessibilityNodeInfo, + interacted: Boolean, + ): AccessibilityNodeEntity? { val interactionTypes = source.actionList.mapNotNull { action -> NodeInteractionType.entries.find { it.accessibilityActionId == action.id } }.distinct() @@ -125,6 +129,17 @@ class AccessibilityNodeRecorder( null }, actions = interactionTypes.toSet(), + interacted = interacted, + tooltip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + source.tooltipText?.toString() + } else { + null + }, + hint = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + source.hintText?.toString() + } else { + null + }, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index d0a3347b75..73931e1160 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -13,15 +13,14 @@ import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.mappings.FingerprintGestureType -import io.github.sds100.keymapper.mappings.FingerprintGesturesSupportedUseCase -import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCase -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.DpadMotionEventTracker -import io.github.sds100.keymapper.mappings.keymaps.detection.KeyMapController -import io.github.sds100.keymapper.mappings.keymaps.detection.TriggerKeyMapFromOtherAppsController -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.keymaps.FingerprintGestureType +import io.github.sds100.keymapper.keymaps.FingerprintGesturesSupportedUseCase +import io.github.sds100.keymapper.keymaps.PauseKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.detection.DetectScreenOffKeyEventsController +import io.github.sds100.keymapper.keymaps.detection.DpadMotionEventTracker +import io.github.sds100.keymapper.keymaps.detection.KeyMapController +import io.github.sds100.keymapper.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 @@ -30,6 +29,7 @@ import io.github.sds100.keymapper.system.inputevents.MyKeyEvent import io.github.sds100.keymapper.system.inputevents.MyMotionEvent import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.util.ServiceEvent import io.github.sds100.keymapper.util.firstBlocking import kotlinx.coroutines.CoroutineScope @@ -85,6 +85,8 @@ abstract class BaseAccessibilityServiceController( * How long should the accessibility service record a trigger in seconds. */ private const val RECORD_TRIGGER_TIMER_LENGTH = 5 + + private const val DEFAULT_NOTIFICATION_TIMEOUT = 200L } private val triggerKeyMapFromOtherAppsController = TriggerKeyMapFromOtherAppsController( @@ -107,7 +109,7 @@ abstract class BaseAccessibilityServiceController( ) private val accessibilityNodeRecorder: AccessibilityNodeRecorder = - AccessibilityNodeRecorder(nodeRepository) + AccessibilityNodeRecorder(nodeRepository, service) private var recordingTriggerJob: Job? = null private val recordingTrigger: Boolean @@ -179,6 +181,9 @@ abstract class BaseAccessibilityServiceController( val serviceEventTypes: MutableStateFlow = MutableStateFlow(AccessibilityEvent.TYPE_WINDOWS_CHANGED) + private val serviceNotificationTimeout: MutableStateFlow = + MutableStateFlow(DEFAULT_NOTIFICATION_TIMEOUT) + init { serviceFlags.onEach { flags -> @@ -202,6 +207,13 @@ abstract class BaseAccessibilityServiceController( } }.launchIn(coroutineScope) + serviceNotificationTimeout.onEach { timeout -> + // check that it isn't null because this can only be called once the service is bound + if (service.notificationTimeout != null) { + service.notificationTimeout = timeout + } + }.launchIn(coroutineScope) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { combine( detectKeyMapsUseCase.requestFingerprintGestureDetection, @@ -274,11 +286,8 @@ abstract class BaseAccessibilityServiceController( val imeInputFocusEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED - val recordNodeEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or - AccessibilityEvent.TYPE_VIEW_CLICKED or - AccessibilityEvent.TYPE_VIEW_LONG_CLICKED or - AccessibilityEvent.TYPE_VIEW_SELECTED or - AccessibilityEvent.TYPE_VIEW_SCROLLED + val recordNodeEvents = + AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED coroutineScope.launch { combine( @@ -304,6 +313,14 @@ abstract class BaseAccessibilityServiceController( newEventTypes } + + serviceNotificationTimeout.update { + if (recordState is RecordAccessibilityNodeState.CountingDown) { + 0L + } else { + DEFAULT_NOTIFICATION_TIMEOUT + } + } }.collect() } } @@ -312,6 +329,7 @@ abstract class BaseAccessibilityServiceController( service.serviceFlags = serviceFlags.value service.serviceFeedbackType = serviceFeedbackType.value service.serviceEventTypes = serviceEventTypes.value + service.notificationTimeout = serviceNotificationTimeout.value // check if fingerprint gestures are supported if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt index 59d9ac5e42..4e45865eb9 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt @@ -40,6 +40,7 @@ interface IAccessibilityService { var serviceFlags: Int? var serviceFeedbackType: Int? var serviceEventTypes: Int? + var notificationTimeout: Long? fun performActionOnNode( findNode: (node: AccessibilityNodeModel) -> Boolean, diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index 99205867b6..0610083100 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -22,25 +22,22 @@ import androidx.lifecycle.LifecycleRegistry import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner -import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.ServiceLocator import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.api.KeyEventRelayService import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapperImpl -import io.github.sds100.keymapper.mappings.FingerprintGestureType -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.keymaps.FingerprintGestureType import io.github.sds100.keymapper.system.devices.InputDeviceUtils import io.github.sds100.keymapper.system.inputevents.MyKeyEvent import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.trigger.KeyEventDetectionSource import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Inject import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.MathUtils import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.Success -import io.github.sds100.keymapper.util.onSuccess import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -124,6 +121,16 @@ class MyAccessibilityService : } } + override var notificationTimeout: Long? + get() = serviceInfo?.notificationTimeout + set(value) { + if (serviceInfo != null && value != null) { + serviceInfo = serviceInfo.apply { + notificationTimeout = value + } + } + } + private val relayServiceCallback: IKeyEventRelayServiceCallback = object : IKeyEventRelayServiceCallback.Stub() { override fun onKeyEvent(event: KeyEvent?): Boolean { @@ -140,6 +147,7 @@ class MyAccessibilityService : scanCode = event.scanCode, device = device, repeatCount = event.repeatCount, + source = event.source, ), ) } @@ -191,14 +199,6 @@ class MyAccessibilityService : override fun onServiceConnected() { super.onServiceConnected() - val inputMethodAdapter = ServiceLocator.inputMethodAdapter(this) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - inputMethodAdapter.getInfoByPackageName(Constants.PACKAGE_NAME).onSuccess { - softKeyboardController.setInputMethodEnabled(it.id, true) - softKeyboardController.switchToInputMethod(it.id) - } - } Timber.i("Accessibility service: onServiceConnected") lifecycleRegistry.currentState = Lifecycle.State.STARTED @@ -315,6 +315,7 @@ class MyAccessibilityService : scanCode = event.scanCode, device = device, repeatCount = event.repeatCount, + source = event.source, ), KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index daa1799a9a..f593683c97 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -125,8 +125,8 @@ class AndroidDevicesAdapter( } override fun getInputDeviceName(descriptor: String): Result { - InputDevice.getDeviceIds().forEach { - val device = InputDevice.getDevice(it) ?: return@forEach + for (id in InputDevice.getDeviceIds()) { + val device = InputDevice.getDevice(id) ?: continue if (device.descriptor == descriptor) { return Success(device.name) @@ -139,8 +139,8 @@ class AndroidDevicesAdapter( private fun updateInputDevices() { val devices = mutableListOf() - InputDevice.getDeviceIds().forEach { - val device = InputDevice.getDevice(it) ?: return@forEach + for (id in InputDevice.getDeviceIds()) { + val device = InputDevice.getDevice(id) ?: continue devices.add(InputDeviceUtils.createInputDeviceInfo(device)) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/files/FileUtils.kt b/app/src/main/java/io/github/sds100/keymapper/system/files/FileUtils.kt index d2a4b14628..d870339f02 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/files/FileUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/files/FileUtils.kt @@ -26,6 +26,7 @@ object FileUtils { const val MIME_TYPE_AUDIO = "audio/*" const val MIME_TYPE_ZIP = "application/zip" const val MIME_TYPE_JSON = "text/json" + const val MIME_TYPE_TEXT = "text/plain" @SuppressLint("SimpleDateFormat") fun createFileDate(): String { diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt index a126bac316..5a680f9893 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt @@ -1,7 +1,27 @@ package io.github.sds100.keymapper.system.inputevents +import android.view.KeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel interface InputEventInjector { suspend fun inputKeyEvent(model: InputKeyModel) + + fun createInjectedKeyEvent( + eventTime: Long, + action: Int, + model: InputKeyModel, + ): KeyEvent { + return KeyEvent( + eventTime, + eventTime, + action, + model.keyCode, + model.repeat, + model.metaState, + model.deviceId, + model.scanCode, + 0, + model.source, + ) + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt index d5b336eabc..a42c1949fa 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt @@ -830,4 +830,43 @@ object InputEventUtils { fun isDpadDevice(event: InputEvent): Boolean = // Check that input comes from a device with directional pads. event.source and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD + + fun isGamepadButton(keyCode: Int): Boolean { + return when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_C, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BUTTON_Z, + KeyEvent.KEYCODE_BUTTON_L1, + KeyEvent.KEYCODE_BUTTON_R1, + KeyEvent.KEYCODE_BUTTON_L2, + KeyEvent.KEYCODE_BUTTON_R2, + KeyEvent.KEYCODE_BUTTON_THUMBL, + KeyEvent.KEYCODE_BUTTON_THUMBR, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_BUTTON_MODE, + KeyEvent.KEYCODE_BUTTON_1, + KeyEvent.KEYCODE_BUTTON_2, + KeyEvent.KEYCODE_BUTTON_3, + KeyEvent.KEYCODE_BUTTON_4, + KeyEvent.KEYCODE_BUTTON_5, + KeyEvent.KEYCODE_BUTTON_6, + KeyEvent.KEYCODE_BUTTON_7, + KeyEvent.KEYCODE_BUTTON_8, + KeyEvent.KEYCODE_BUTTON_9, + KeyEvent.KEYCODE_BUTTON_10, + KeyEvent.KEYCODE_BUTTON_11, + KeyEvent.KEYCODE_BUTTON_12, + KeyEvent.KEYCODE_BUTTON_13, + KeyEvent.KEYCODE_BUTTON_14, + KeyEvent.KEYCODE_BUTTON_15, + KeyEvent.KEYCODE_BUTTON_16, + -> true + + else -> false + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt index 64384acaac..6517eefa26 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt @@ -9,4 +9,5 @@ data class MyKeyEvent( val scanCode: Int, val device: InputDeviceInfo?, val repeatCount: Int, + val source: Int, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AutoSwitchImeController.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AutoSwitchImeController.kt index 63f41376a0..cf1a2dcca1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AutoSwitchImeController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AutoSwitchImeController.kt @@ -4,7 +4,7 @@ import io.github.sds100.keymapper.R import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.system.accessibility.ServiceAdapter import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.popup.PopupMessageAdapter diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt index 756acde8df..e5ce73abdd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt @@ -82,7 +82,7 @@ class ImeInputEventInjectorImpl( val eventTime = SystemClock.uptimeMillis() - val keyEvent = createKeyEvent(eventTime, action, model) + val keyEvent = createInjectedKeyEvent(eventTime, action, model) putExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT, keyEvent) @@ -90,34 +90,19 @@ class ImeInputEventInjectorImpl( } } - private fun createKeyEvent( - eventTime: Long, - action: Int, - model: InputKeyModel, - ): KeyEvent = KeyEvent( - eventTime, - eventTime, - action, - model.keyCode, - model.repeat, - model.metaState, - model.deviceId, - model.scanCode, - ) - private fun inputKeyEventRelayService(model: InputKeyModel, imePackageName: String) { val eventTime = SystemClock.uptimeMillis() when (model.inputType) { InputEventType.DOWN_UP -> { - val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) + val downKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) keyEventRelayService.sendKeyEvent( downKeyEvent, imePackageName, KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, ) - val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) + val upKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_UP, model) keyEventRelayService.sendKeyEvent( upKeyEvent, imePackageName, @@ -126,7 +111,7 @@ class ImeInputEventInjectorImpl( } InputEventType.DOWN -> { - val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) + val downKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) keyEventRelayService.sendKeyEvent( downKeyEvent, imePackageName, @@ -135,7 +120,7 @@ class ImeInputEventInjectorImpl( } InputEventType.UP -> { - val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) + val upKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_UP, model) keyEventRelayService.sendKeyEvent( upKeyEvent, imePackageName, diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt index ec2942ec0e..17e4a53871 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.system.inputmethod +import android.view.InputDevice import io.github.sds100.keymapper.util.InputEventType /** @@ -12,4 +13,5 @@ data class InputKeyModel( val deviceId: Int = 0, val scanCode: Int = 0, val repeat: Int = 0, + val source: Int = InputDevice.SOURCE_UNKNOWN, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt index 7fc9bebaf7..3886863a38 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt @@ -82,6 +82,34 @@ class AndroidMediaAdapter(context: Context, coroutineScope: CoroutineScope) : Me override fun nextTrack(packageName: String?): Result<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT, packageName) + override fun stop(packageName: String?): Result<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP, packageName) + + override fun stopFileMedia(): Result<*> { + synchronized(mediaPlayerLock) { + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + } + + return Success(Unit) + } + + override fun stepForward(packageName: String?): Result<*> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STEP_FORWARD, packageName) + } else { + return Error.SdkVersionTooLow(Build.VERSION_CODES.M) + } + } + + override fun stepBackward(packageName: String?): Result<*> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STEP_BACKWARD, packageName) + } else { + return Error.SdkVersionTooLow(Build.VERSION_CODES.M) + } + } + override fun getActiveMediaSessionPackages(): List { return activeMediaSessions.value .filter { it.playbackState?.state == PlaybackState.STATE_PLAYING } @@ -105,7 +133,7 @@ class AndroidMediaAdapter(context: Context, coroutineScope: CoroutineScope) : Me return audioVolumeControlStreams } - override fun playSoundFile(uri: String, stream: VolumeStream): Result<*> { + override fun playFile(uri: String, stream: VolumeStream): Result<*> { try { synchronized(mediaPlayerLock) { mediaPlayer?.stop() @@ -147,16 +175,6 @@ class AndroidMediaAdapter(context: Context, coroutineScope: CoroutineScope) : Me } } - override fun stopMedia(): Result<*> { - synchronized(mediaPlayerLock) { - mediaPlayer?.stop() - mediaPlayer?.release() - mediaPlayer = null - } - - return Success(Unit) - } - fun onActiveMediaSessionChange(mediaSessions: List) { activeMediaSessions.update { mediaSessions } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt index d990c9afe5..c0a4858e81 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt @@ -35,7 +35,10 @@ interface MediaAdapter { fun playPause(packageName: String? = null): Result<*> fun previousTrack(packageName: String? = null): Result<*> fun nextTrack(packageName: String? = null): Result<*> + fun stop(packageName: String? = null): Result<*> + fun stepForward(packageName: String? = null): Result<*> + fun stepBackward(packageName: String? = null): Result<*> - fun playSoundFile(uri: String, stream: VolumeStream): Result<*> - fun stopMedia(): Result<*> + fun playFile(uri: String, stream: VolumeStream): Result<*> + fun stopFileMedia(): Result<*> } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt b/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt index 0864806c2d..71deee9d52 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt @@ -46,7 +46,7 @@ class OpenMenuHelper( return success() } - suAdapter.isGranted.firstBlocking() -> + suAdapter.isRooted.firstBlocking() -> return suAdapter.execute("input keyevent ${KeyEvent.KEYCODE_MENU}\n") else -> { 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 63ffafbdab..0f59556bd1 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 @@ -23,7 +23,7 @@ class ManageNotificationsUseCaseImpl( override val showImePickerNotification: Flow = combine( - suAdapter.isGranted, + suAdapter.isRooted, preferences.get(Keys.showImePickerNotification), ) { hasRootPermission, show -> when { 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 b3168fd813..15005576bb 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 @@ -6,7 +6,7 @@ import androidx.core.app.NotificationManagerCompat import io.github.sds100.keymapper.BaseMainActivity import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.system.accessibility.ServiceState 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 4b7e63735f..c124e7283d 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 @@ -100,7 +100,7 @@ class AndroidPermissionAdapter( .stateIn(coroutineScope, SharingStarted.Eagerly, false) init { - suAdapter.isGranted + suAdapter.isRooted .drop(1) .onEach { onPermissionsChanged() } .launchIn(coroutineScope) @@ -278,7 +278,7 @@ class AndroidPermissionAdapter( Manifest.permission.CALL_PHONE, ) == PERMISSION_GRANTED - Permission.ROOT -> suAdapter.isGranted.value + Permission.ROOT -> suAdapter.isRooted.value Permission.IGNORE_BATTERY_OPTIMISATION -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 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 d6aee9226c..e3f4b247bb 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 @@ -11,6 +11,7 @@ import android.provider.Settings import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat import androidx.navigation.NavController import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.NavAppDirections @@ -98,7 +99,23 @@ class RequestPermissionDelegate( requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) Permission.POST_NOTIFICATIONS -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + val showRationale = ActivityCompat.shouldShowRequestPermissionRationale( + activity, + Manifest.permission.POST_NOTIFICATIONS, + ) + + // The system will say you have to show a rationale if the user previously + // denied the permission. Therefore, the permission dialog will not show and so + // open the notification settings to turn it on manually. + if (showRationale) { + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, Constants.PACKAGE_NAME) + + activity.startActivity(this) + } + } else { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/ringtones/RingtoneAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/ringtones/RingtoneAdapter.kt new file mode 100644 index 0000000000..ae036aaa7e --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/system/ringtones/RingtoneAdapter.kt @@ -0,0 +1,79 @@ +package io.github.sds100.keymapper.system.ringtones + +import android.content.Context +import android.media.Ringtone +import android.media.RingtoneManager +import android.os.Build +import androidx.core.net.toUri +import io.github.sds100.keymapper.util.Error +import io.github.sds100.keymapper.util.Result +import io.github.sds100.keymapper.util.Success + +class AndroidRingtoneAdapter(context: Context) : RingtoneAdapter { + private val ctx: Context = context.applicationContext + private val ringtoneManager: RingtoneManager by lazy { + RingtoneManager(ctx).apply { + setType(RingtoneManager.TYPE_ALL) + stopPreviousRingtone = true + } + } + + private val lock = Any() + private var playingRingtone: Ringtone? = null + + override fun getLabel(uri: String): Result { + val ringtone = getRingtone(uri) + + if (ringtone == null) { + return Error.CantFindSoundFile + } + + return Success(ringtone.getTitle(ctx)) + } + + override fun exists(uri: String): Boolean { + return getRingtone(uri) != null + } + + override fun play(uri: String): Result { + val ringtone = getRingtone(uri) + + if (ringtone == null) { + return Error.CantFindSoundFile + } else { + ringtoneManager.stopPreviousRingtone() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ringtone.isLooping = false + } + + synchronized(lock) { + playingRingtone?.stop() + playingRingtone = ringtone + ringtone.play() + } + + return Success(Unit) + } + } + + override fun stopPlaying() { + ringtoneManager.stopPreviousRingtone() + + synchronized(lock) { + playingRingtone?.stop() + playingRingtone = null + } + } + + private fun getRingtone(uri: String): Ringtone? { + return RingtoneManager.getRingtone(ctx, uri.toUri()) + } +} + +interface RingtoneAdapter { + fun getLabel(uri: String): Result + fun exists(uri: String): Boolean + fun play(uri: String): Result + fun stopPlaying() +} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt index 7b9db9f99e..9094681d3b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt @@ -1,18 +1,17 @@ package io.github.sds100.keymapper.system.root -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.Shell +import com.topjohnwu.superuser.Shell +import io.github.sds100.keymapper.system.SimpleShell import io.github.sds100.keymapper.system.permissions.Permission 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.firstBlocking import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import java.io.IOException import java.io.InputStream @@ -22,41 +21,32 @@ import java.io.InputStream class SuAdapterImpl( coroutineScope: CoroutineScope, - private val preferenceRepository: PreferenceRepository, ) : SuAdapter { private var process: Process? = null - override val isGranted: StateFlow = preferenceRepository.get(Keys.hasRootPermission) - .map { it ?: false } - .stateIn(coroutineScope, SharingStarted.Eagerly, false) + override val isRooted: MutableStateFlow = MutableStateFlow(false) - override fun requestPermission(): Boolean { - preferenceRepository.set(Keys.hasRootPermission, true) + init { + invalidateIsRooted() + } + override fun requestPermission(): Boolean { // show the su prompt - Shell.run("su") + Shell.getShell() - return true + return isRooted.updateAndGet { Shell.isAppGrantedRoot() ?: false } } override fun execute(command: String, block: Boolean): Result<*> { - if (!isGranted.firstBlocking()) { + if (!isRooted.firstBlocking()) { return Error.PermissionDenied(Permission.ROOT) } try { if (block) { - // Don't use the long running su process because that will block the thread indefinitely - Shell.run("su", "-c", command, waitFor = true) + Shell.cmd(command).exec() } else { - if (process == null) { - process = ProcessBuilder("su").start() - } - - with(process!!.outputStream.bufferedWriter()) { - write("$command\n") - flush() - } + Shell.cmd(command).submit() } return Success(Unit) @@ -66,21 +56,26 @@ class SuAdapterImpl( } override fun getCommandOutput(command: String): Result { - if (!isGranted.firstBlocking()) { + if (!isRooted.firstBlocking()) { return Error.PermissionDenied(Permission.ROOT) } try { - val inputStream = Shell.getShellCommandStdOut("su", "-c", command) + val inputStream = SimpleShell.getShellCommandStdOut("su", "-c", command) return Success(inputStream) } catch (e: IOException) { return Error.UnknownIOError } } + + fun invalidateIsRooted() { + Shell.getShell() + isRooted.update { Shell.isAppGrantedRoot() ?: false } + } } interface SuAdapter { - val isGranted: StateFlow + val isRooted: StateFlow /** * @return whether root permission was granted successfully 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/trigger/AssistantTriggerKey.kt similarity index 96% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/AssistantTriggerKey.kt index 45e37e94e4..1be79413e4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/AssistantTriggerKey.kt @@ -1,8 +1,8 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.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 io.github.sds100.keymapper.keymaps.ClickType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.util.UUID 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/trigger/AssistantTriggerType.kt similarity index 90% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerType.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/AssistantTriggerType.kt index f82c6ad9b0..57a5b82736 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerType.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/AssistantTriggerType.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger /** * The type of assistant that triggers an assistant trigger key. The voice assistant diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/BaseConfigTriggerViewModel.kt similarity index 93% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/BaseConfigTriggerViewModel.kt index 34658e5890..21a500ecdc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/BaseConfigTriggerViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import android.view.KeyEvent import androidx.compose.material.icons.Icons @@ -9,15 +9,16 @@ 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.FingerprintGestureType -import io.github.sds100.keymapper.mappings.FingerprintGesturesSupportedUseCase -import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapOptionsViewModel -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.mappings.keymaps.ShortcutModel +import io.github.sds100.keymapper.keymaps.ClickType +import io.github.sds100.keymapper.keymaps.ConfigKeyMapOptionsViewModel +import io.github.sds100.keymapper.keymaps.ConfigKeyMapUseCase +import io.github.sds100.keymapper.keymaps.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.keymaps.DisplayKeyMapUseCase +import io.github.sds100.keymapper.keymaps.FingerprintGestureType +import io.github.sds100.keymapper.keymaps.FingerprintGesturesSupportedUseCase +import io.github.sds100.keymapper.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.ShortcutModel +import io.github.sds100.keymapper.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.purchasing.ProductId import io.github.sds100.keymapper.purchasing.PurchasingManager @@ -43,6 +44,7 @@ import io.github.sds100.keymapper.util.ui.ViewModelHelper import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.util.ui.showPopup import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -174,6 +176,13 @@ abstract class BaseConfigTriggerViewModel( private var isRecordingCompletionUserInitiated: Boolean = false init { + val showTapTargetsPairFlow: Flow> = combine( + onboarding.showTapTarget(OnboardingTapTarget.RECORD_TRIGGER), + onboarding.showTapTarget(OnboardingTapTarget.ADVANCED_TRIGGERS), + ) { recordTriggerTapTarget, advancedTriggersTapTarget -> + Pair(recordTriggerTapTarget, advancedTriggersTapTarget) + } + // IMPORTANT! Do not flow on another thread because this causes the drag and drop // animations to be more janky. combine( @@ -181,15 +190,16 @@ abstract class BaseConfigTriggerViewModel( config.keyMap, displayKeyMap.showDeviceDescriptors, triggerKeyShortcuts, - onboarding.hasViewedAdvancedTriggers, - ) { triggerErrorSnapshot, keyMap, showDeviceDescriptors, shortcuts, viewedAdvancedTriggers -> + showTapTargetsPairFlow, + ) { triggerErrorSnapshot, keyMap, showDeviceDescriptors, shortcuts, showTapTargetsPair -> _state.update { buildUiState( keyMap, showDeviceDescriptors, shortcuts, triggerErrorSnapshot, - viewedAdvancedTriggers, + showTapTargetsPair.first, + showTapTargetsPair.second, ) } }.launchIn(coroutineScope) @@ -255,7 +265,8 @@ abstract class BaseConfigTriggerViewModel( showDeviceDescriptors: Boolean, triggerKeyShortcuts: Set>, triggerErrorSnapshot: TriggerErrorSnapshot, - viewedAdvancedTriggers: Boolean, + showRecordTriggerTapTarget: Boolean, + showAdvancedTriggersTapTarget: Boolean, ): State { return keyMapState.mapData { keyMap -> val trigger = keyMap.trigger @@ -263,7 +274,8 @@ abstract class BaseConfigTriggerViewModel( if (trigger.keys.isEmpty()) { return@mapData ConfigTriggerState.Empty( triggerKeyShortcuts, - !viewedAdvancedTriggers, + showRecordTriggerTapTarget = showRecordTriggerTapTarget, + showAdvancedTriggersTapTarget = showAdvancedTriggersTapTarget, ) } @@ -316,7 +328,7 @@ abstract class BaseConfigTriggerViewModel( triggerModeButtonsVisible = triggerModeButtonsVisible, checkedTriggerMode = trigger.mode, shortcuts = triggerKeyShortcuts, - showNewBadge = !viewedAdvancedTriggers, + showAdvancedTriggersTapTarget = showAdvancedTriggersTapTarget, ) } } @@ -772,15 +784,28 @@ abstract class BaseConfigTriggerViewModel( fun onNeverShowNoKeysRecordedClick() { onboarding.neverShowNoKeysRecordedBottomSheet() } + + fun onRecordTriggerTapTargetCompleted() { + onboarding.completedTapTarget(OnboardingTapTarget.RECORD_TRIGGER) + } + + fun onSkipTapTargetClick() { + onboarding.skipTapTargetOnboarding() + } + + fun onAdvancedTriggersTapTargetCompleted() { + onboarding.completedTapTarget(OnboardingTapTarget.ADVANCED_TRIGGERS) + } } sealed class ConfigTriggerState { abstract val shortcuts: Set> - abstract val showNewBadge: Boolean + abstract val showAdvancedTriggersTapTarget: Boolean data class Empty( override val shortcuts: Set> = emptySet(), - override val showNewBadge: Boolean, + val showRecordTriggerTapTarget: Boolean = false, + override val showAdvancedTriggersTapTarget: Boolean = false, ) : ConfigTriggerState() data class Loaded( @@ -792,7 +817,7 @@ sealed class ConfigTriggerState { val triggerModeButtonsEnabled: Boolean = false, val triggerModeButtonsVisible: Boolean = false, override val shortcuts: Set> = emptySet(), - override val showNewBadge: Boolean, + override val showAdvancedTriggersTapTarget: Boolean = false, ) : ConfigTriggerState() } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ChooseTriggerKeyDeviceModel.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/ChooseTriggerKeyDeviceModel.kt similarity index 72% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ChooseTriggerKeyDeviceModel.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/ChooseTriggerKeyDeviceModel.kt index 7189f03d95..4ffb1bae0a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/ChooseTriggerKeyDeviceModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/ChooseTriggerKeyDeviceModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger /** * Created by sds100 on 07/03/2021. diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FingerprintTriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/FingerprintTriggerKey.kt similarity index 94% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FingerprintTriggerKey.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/FingerprintTriggerKey.kt index 1ec7fae6f4..a5240ff1da 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FingerprintTriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/FingerprintTriggerKey.kt @@ -1,9 +1,9 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import io.github.sds100.keymapper.data.entities.FingerprintTriggerKeyEntity import io.github.sds100.keymapper.data.entities.TriggerKeyEntity -import io.github.sds100.keymapper.mappings.ClickType -import io.github.sds100.keymapper.mappings.FingerprintGestureType +import io.github.sds100.keymapper.keymaps.ClickType +import io.github.sds100.keymapper.keymaps.FingerprintGestureType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.util.UUID diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FloatingButtonKey.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/FloatingButtonKey.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FloatingButtonKey.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/FloatingButtonKey.kt index ca23629e92..a316f326fa 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FloatingButtonKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/FloatingButtonKey.kt @@ -1,11 +1,11 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout import io.github.sds100.keymapper.data.entities.FloatingButtonKeyEntity import io.github.sds100.keymapper.data.entities.TriggerKeyEntity import io.github.sds100.keymapper.floating.FloatingButtonData import io.github.sds100.keymapper.floating.FloatingButtonEntityMapper -import io.github.sds100.keymapper.mappings.ClickType +import io.github.sds100.keymapper.keymaps.ClickType import kotlinx.serialization.Serializable import java.util.UUID 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/trigger/KeyCodeTriggerKey.kt similarity index 93% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/KeyCodeTriggerKey.kt index 94b8c0f579..91d5096ac2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/KeyCodeTriggerKey.kt @@ -1,8 +1,8 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.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 io.github.sds100.keymapper.keymaps.ClickType import kotlinx.serialization.Serializable import splitties.bitflags.hasFlag import splitties.bitflags.withFlag @@ -89,11 +89,12 @@ data class KeyCodeTriggerKey( TriggerKeyDevice.Internal -> KeyCodeTriggerKeyEntity.DEVICE_ID_THIS_DEVICE } - val deviceName = if (key.device is TriggerKeyDevice.External) { - key.device.name - } else { - null - } + val deviceName = + if (key.device is TriggerKeyDevice.External) { + key.device.name + } else { + null + } val clickType = when (key.clickType) { ClickType.SHORT_PRESS -> TriggerKeyEntity.SHORT_PRESS diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyEventDetectionSource.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/KeyEventDetectionSource.kt similarity index 58% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyEventDetectionSource.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/KeyEventDetectionSource.kt index b1624e368f..66f147dfc8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyEventDetectionSource.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/KeyEventDetectionSource.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger enum class KeyEventDetectionSource { ACCESSIBILITY_SERVICE, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapListItemModel.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/KeyMapListItemModel.kt similarity index 92% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapListItemModel.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/KeyMapListItemModel.kt index 95ebd9ba69..accb6bf73b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyMapListItemModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/KeyMapListItemModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import androidx.compose.ui.graphics.vector.ImageVector import io.github.sds100.keymapper.constraints.ConstraintMode diff --git a/app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerButtonRow.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerButtonRow.kt new file mode 100644 index 0000000000..eead8308f3 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerButtonRow.kt @@ -0,0 +1,186 @@ +package io.github.sds100.keymapper.trigger + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.canopas.lib.showcase.IntroShowcase +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.compose.LocalCustomColorsPalette +import io.github.sds100.keymapper.onboarding.OnboardingTapTarget +import io.github.sds100.keymapper.util.ui.compose.KeyMapperTapTarget +import io.github.sds100.keymapper.util.ui.compose.keyMapperShowcaseStyle + +@Composable +fun RecordTriggerButtonRow( + modifier: Modifier = Modifier, + onRecordTriggerClick: () -> Unit = {}, + recordTriggerState: RecordTriggerState, + onAdvancedTriggersClick: () -> Unit = {}, + showRecordTriggerTapTarget: Boolean = false, + onRecordTriggerTapTargetCompleted: () -> Unit = {}, + onSkipTapTarget: () -> Unit = {}, + showAdvancedTriggerTapTarget: Boolean = false, + onAdvancedTriggerTapTargetCompleted: () -> Unit = {}, +) { + Row(modifier, verticalAlignment = Alignment.CenterVertically) { + IntroShowcase( + showIntroShowCase = showRecordTriggerTapTarget, + onShowCaseCompleted = onRecordTriggerTapTargetCompleted, + dismissOnClickOutside = true, + ) { + RecordTriggerButton( + modifier = Modifier + .weight(1f) + .introShowCaseTarget(0, style = keyMapperShowcaseStyle()) { + KeyMapperTapTarget( + OnboardingTapTarget.RECORD_TRIGGER, + onSkipClick = onSkipTapTarget, + ) + }, + recordTriggerState, + onClick = onRecordTriggerClick, + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + IntroShowcase( + showIntroShowCase = showAdvancedTriggerTapTarget, + onShowCaseCompleted = onAdvancedTriggerTapTargetCompleted, + dismissOnClickOutside = true, + ) { + AdvancedTriggersButton( + modifier = Modifier + .weight(1f) + .introShowCaseTarget(0, style = keyMapperShowcaseStyle()) { + KeyMapperTapTarget( + OnboardingTapTarget.ADVANCED_TRIGGERS, + showSkipButton = false, + ) + }, + isEnabled = recordTriggerState !is RecordTriggerState.CountingDown, + 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) + + else -> + stringResource(R.string.button_record_trigger) + } + + FilledTonalButton( + modifier = modifier, + onClick = onClick, + colors = colors, + ) { + BasicText( + text = text, + maxLines = 1, + autoSize = TextAutoSize.StepBased( + minFontSize = 5.sp, + maxFontSize = MaterialTheme.typography.labelLarge.fontSize, + ), + style = MaterialTheme.typography.labelLarge, + color = { colors.contentColor }, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun AdvancedTriggersButton( + modifier: Modifier, + isEnabled: Boolean, + onClick: () -> Unit, +) { + OutlinedButton( + modifier = modifier, + enabled = isEnabled, + onClick = onClick, + ) { + val color = ButtonDefaults.textButtonColors().contentColor + BasicText( + text = stringResource(R.string.button_advanced_triggers), + maxLines = 1, + autoSize = TextAutoSize.StepBased( + minFontSize = 5.sp, + maxFontSize = MaterialTheme.typography.labelLarge.fontSize, + ), + style = MaterialTheme.typography.labelLarge, + color = { color }, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@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.Idle, + ) + } + } +} + +@Preview(widthDp = 300) +@Composable +private fun PreviewStoppedCompact() { + KeyMapperTheme { + Surface { + RecordTriggerButtonRow( + modifier = Modifier.fillMaxWidth(), + recordTriggerState = RecordTriggerState.Idle, + ) + } + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerState.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerState.kt similarity index 85% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerState.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerState.kt index 2a0c34f462..afaad64fad 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerState.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerState.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger /** * Created by sds100 on 04/03/2021. diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerUseCase.kt similarity index 96% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerUseCase.kt index 68b783ef83..a9e5730ca2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordTriggerUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/RecordTriggerUseCase.kt @@ -1,7 +1,7 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import android.view.KeyEvent -import io.github.sds100.keymapper.mappings.keymaps.detection.DpadMotionEventTracker +import io.github.sds100.keymapper.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.system.accessibility.ServiceAdapter import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.system.inputevents.InputEventUtils diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordedKey.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/RecordedKey.kt similarity index 74% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordedKey.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/RecordedKey.kt index a6eb4e92d9..17cbc6c856 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/RecordedKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/RecordedKey.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger /** * Created by sds100 on 04/03/2021. diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardBottomSheet.kt similarity index 99% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardBottomSheet.kt index 51c21bd786..b6582cbee3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardBottomSheet.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardState.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardState.kt similarity index 85% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardState.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardState.kt index 4170cdb35a..c21861f62c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardState.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardState.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger data class SetupGuiKeyboardState( val isKeyboardInstalled: Boolean, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardUseCase.kt similarity index 97% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardUseCase.kt index c891d88b8a..759e3dbff6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/SetupGuiKeyboardUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import io.github.sds100.keymapper.system.apps.PackageInfo import io.github.sds100.keymapper.system.apps.PackageManagerAdapter diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/Trigger.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/Trigger.kt index 576ed1fd12..46d94bf956 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/Trigger.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/Trigger.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import io.github.sds100.keymapper.data.entities.AssistantTriggerKeyEntity import io.github.sds100.keymapper.data.entities.EntityExtra @@ -9,7 +9,7 @@ import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity import io.github.sds100.keymapper.data.entities.TriggerEntity import io.github.sds100.keymapper.data.entities.getData import io.github.sds100.keymapper.floating.FloatingButtonEntityMapper -import io.github.sds100.keymapper.mappings.ClickType +import io.github.sds100.keymapper.keymaps.ClickType import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.valueOrNull import kotlinx.serialization.Serializable @@ -53,7 +53,10 @@ data class Trigger( fun isDetectingWhenScreenOffAllowed(): Boolean { return keys.isNotEmpty() && keys.all { - it is KeyCodeTriggerKey && InputEventUtils.canDetectKeyWhenScreenOff(it.keyCode) + it is KeyCodeTriggerKey && + InputEventUtils.canDetectKeyWhenScreenOff( + it.keyCode, + ) } } @@ -90,7 +93,9 @@ object TriggerEntityMapper { val keys = entity.keys.map { key -> when (key) { is AssistantTriggerKeyEntity -> AssistantTriggerKey.fromEntity(key) - is KeyCodeTriggerKeyEntity -> KeyCodeTriggerKey.fromEntity(key) + is KeyCodeTriggerKeyEntity -> KeyCodeTriggerKey.fromEntity( + key, + ) is FloatingButtonKeyEntity -> { val floatingButton = floatingButtons.find { it.button.uid == key.buttonUid } FloatingButtonKey.fromEntity(key, floatingButton) @@ -203,7 +208,9 @@ object TriggerEntityMapper { val keys = trigger.keys.map { key -> when (key) { is AssistantTriggerKey -> AssistantTriggerKey.toEntity(key) - is KeyCodeTriggerKey -> KeyCodeTriggerKey.toEntity(key) + is KeyCodeTriggerKey -> KeyCodeTriggerKey.toEntity( + key, + ) is FloatingButtonKey -> FloatingButtonKey.toEntity(key) is FingerprintTriggerKey -> FingerprintTriggerKey.toEntity(key) } 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/trigger/TriggerError.kt similarity index 91% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerError.kt index a3fa5cc5e1..1d00b9fa36 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerError.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger /** * Created by sds100 on 04/04/2021. diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerErrorSnapshot.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerErrorSnapshot.kt similarity index 83% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerErrorSnapshot.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerErrorSnapshot.kt index 7b4fe3910d..5aac124b17 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerErrorSnapshot.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerErrorSnapshot.kt @@ -1,9 +1,9 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import android.os.Build import android.view.KeyEvent -import io.github.sds100.keymapper.mappings.keymaps.KeyMap -import io.github.sds100.keymapper.mappings.keymaps.requiresImeKeyEventForwardingInPhoneCall +import io.github.sds100.keymapper.keymaps.KeyMap +import io.github.sds100.keymapper.keymaps.requiresImeKeyEventForwardingInPhoneCall import io.github.sds100.keymapper.purchasing.ProductId import io.github.sds100.keymapper.system.inputevents.InputEventUtils import io.github.sds100.keymapper.util.Error @@ -53,7 +53,8 @@ data class TriggerErrorSnapshot( return TriggerError.CANT_DETECT_IN_PHONE_CALL } - val requiresDndAccess = key is KeyCodeTriggerKey && key.keyCode in keysThatRequireDndAccess + val requiresDndAccess = + key is KeyCodeTriggerKey && key.keyCode in keysThatRequireDndAccess if (requiresDndAccess) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isDndAccessGranted) { @@ -69,7 +70,11 @@ data class TriggerErrorSnapshot( } val containsDpadKey = - key is KeyCodeTriggerKey && InputEventUtils.isDpadKeyCode(key.keyCode) && key.detectionSource == KeyEventDetectionSource.INPUT_METHOD + key is KeyCodeTriggerKey && + InputEventUtils.isDpadKeyCode( + key.keyCode, + ) && + key.detectionSource == KeyEventDetectionSource.INPUT_METHOD if (showDpadImeSetupError && !isKeyMapperImeChosen && containsDpadKey) { return TriggerError.DPAD_IME_NOT_SELECTED 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/trigger/TriggerKey.kt similarity index 89% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKey.kt index a94d0db410..0f6c89a17d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKey.kt @@ -1,6 +1,6 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger -import io.github.sds100.keymapper.mappings.ClickType +import io.github.sds100.keymapper.keymaps.ClickType import kotlinx.serialization.Serializable @Serializable 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/trigger/TriggerKeyDevice.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyDevice.kt index 3c0422ef05..24f6dc8672 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyDevice.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyDevice.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import io.github.sds100.keymapper.system.devices.InputDeviceInfo import kotlinx.serialization.Serializable 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/trigger/TriggerKeyListItem.kt similarity index 98% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyListItem.kt index 46f27f7661..2bc355370a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyListItem.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable @@ -42,10 +42,10 @@ import androidx.compose.ui.text.style.TextOverflow 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.draggable.DragDropState -import io.github.sds100.keymapper.mappings.ClickType -import io.github.sds100.keymapper.mappings.FingerprintGestureType +import io.github.sds100.keymapper.keymaps.ClickType +import io.github.sds100.keymapper.keymaps.FingerprintGestureType import io.github.sds100.keymapper.util.ui.LinkType +import io.github.sds100.keymapper.util.ui.compose.DragDropState @Composable fun TriggerKeyListItem( diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyOptionsBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyOptionsBottomSheet.kt similarity index 98% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyOptionsBottomSheet.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyOptionsBottomSheet.kt index f1fb647ca7..5de6ed7cbc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyOptionsBottomSheet.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -36,8 +36,8 @@ 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.mappings.ClickType -import io.github.sds100.keymapper.mappings.FingerprintGestureType +import io.github.sds100.keymapper.keymaps.ClickType +import io.github.sds100.keymapper.keymaps.FingerprintGestureType import io.github.sds100.keymapper.util.ui.CheckBoxListItem import io.github.sds100.keymapper.util.ui.compose.CheckBoxText import io.github.sds100.keymapper.util.ui.compose.RadioButtonText diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyShortcut.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyShortcut.kt similarity index 61% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyShortcut.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyShortcut.kt index 305ab268dc..26c90c031c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyShortcut.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerKeyShortcut.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger enum class TriggerKeyShortcut { ASSISTANT, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerMode.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerMode.kt similarity index 82% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerMode.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerMode.kt index 2da4ef2551..1c04435521 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerMode.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerMode.kt @@ -1,6 +1,6 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger -import io.github.sds100.keymapper.mappings.ClickType +import io.github.sds100.keymapper.keymaps.ClickType import kotlinx.serialization.Serializable /** @@ -15,7 +15,7 @@ sealed class TriggerMode : Comparable { data class Parallel(val clickType: ClickType) : TriggerMode() { override fun compareTo(other: TriggerMode): Int { if (other !is Parallel) { - return super.compareTo(other) + return super.compareTo(other) } return clickType.compareTo(other.clickType) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerScreen.kt similarity index 85% rename from app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt rename to app/src/main/java/io/github/sds100/keymapper/trigger/TriggerScreen.kt index c56c550109..edbf72bd54 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/TriggerScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps.trigger +package io.github.sds100.keymapper.trigger import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -37,17 +37,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.compose.draggable.DraggableItem -import io.github.sds100.keymapper.compose.draggable.rememberDragDropState -import io.github.sds100.keymapper.mappings.ClickType -import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel -import io.github.sds100.keymapper.mappings.keymaps.ShortcutRow +import io.github.sds100.keymapper.keymaps.ClickType +import io.github.sds100.keymapper.keymaps.ShortcutModel +import io.github.sds100.keymapper.keymaps.ShortcutRow import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.ui.LinkType import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.DraggableItem import io.github.sds100.keymapper.util.ui.compose.RadioButtonText +import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -132,6 +133,9 @@ fun TriggerScreen(modifier: Modifier = Modifier, viewModel: ConfigTriggerViewMod onMoveTriggerKey = viewModel::onMoveTriggerKey, onFixErrorClick = viewModel::onTriggerErrorClick, onClickShortcut = viewModel::onClickTriggerKeyShortcut, + onRecordTriggerTapTargetCompleted = viewModel::onRecordTriggerTapTargetCompleted, + onSkipTapTarget = viewModel::onSkipTapTargetClick, + onAdvancedTriggerTapTargetCompleted = viewModel::onAdvancedTriggersTapTargetCompleted, ) } else { TriggerScreenVertical( @@ -148,6 +152,9 @@ fun TriggerScreen(modifier: Modifier = Modifier, viewModel: ConfigTriggerViewMod onMoveTriggerKey = viewModel::onMoveTriggerKey, onFixErrorClick = viewModel::onTriggerErrorClick, onClickShortcut = viewModel::onClickTriggerKeyShortcut, + onRecordTriggerTapTargetCompleted = viewModel::onRecordTriggerTapTargetCompleted, + onSkipTapTarget = viewModel::onSkipTapTargetClick, + onAdvancedTriggerTapTargetCompleted = viewModel::onAdvancedTriggersTapTargetCompleted, ) } } @@ -161,6 +168,13 @@ private fun isHorizontalLayout(): Boolean { return windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT } +@Composable +private fun isVerticalCompactLayout(): Boolean { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + return windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT && windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT +} + @Composable private fun Loading(modifier: Modifier = Modifier) { Box(modifier = modifier, contentAlignment = Alignment.Center) { @@ -183,6 +197,9 @@ private fun TriggerScreenVertical( onMoveTriggerKey: (fromIndex: Int, toIndex: Int) -> Unit = { _, _ -> }, onFixErrorClick: (TriggerError) -> Unit = {}, onClickShortcut: (TriggerKeyShortcut) -> Unit = {}, + onRecordTriggerTapTargetCompleted: () -> Unit = {}, + onSkipTapTarget: () -> Unit = {}, + onAdvancedTriggerTapTargetCompleted: () -> Unit = {}, ) { Surface(modifier = modifier) { Column { @@ -222,6 +239,7 @@ private fun TriggerScreenVertical( } is ConfigTriggerState.Loaded -> { + val isCompact = isVerticalCompactLayout() Spacer(Modifier.height(8.dp)) TriggerList( @@ -242,16 +260,26 @@ private fun TriggerScreenVertical( clickTypes = configState.clickTypeButtons, checkedClickType = configState.checkedClickType, onSelectClickType = onSelectClickType, + maxLines = if (isCompact) 1 else 2, ) } if (configState.triggerModeButtonsVisible) { + if (!isCompact) { + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = stringResource(R.string.press_dot_dot_dot), + style = MaterialTheme.typography.labelLarge, + ) + } + TriggerModeRadioGroup( modifier = Modifier.padding(horizontal = 8.dp), mode = configState.checkedTriggerMode, isEnabled = configState.triggerModeButtonsEnabled, onSelectParallelMode = onSelectParallelMode, onSelectSequenceMode = onSelectSequenceMode, + maxLines = if (isCompact) 1 else 2, ) } } @@ -264,7 +292,12 @@ private fun TriggerScreenVertical( onRecordTriggerClick = onRecordTriggerClick, recordTriggerState = recordTriggerState, onAdvancedTriggersClick = onAdvancedTriggersClick, - showNewBadge = configState.showNewBadge, + showRecordTriggerTapTarget = (configState as? ConfigTriggerState.Empty)?.showRecordTriggerTapTarget + ?: false, + onRecordTriggerTapTargetCompleted = onRecordTriggerTapTargetCompleted, + onSkipTapTarget = onSkipTapTarget, + showAdvancedTriggerTapTarget = configState.showAdvancedTriggersTapTarget, + onAdvancedTriggerTapTargetCompleted = onAdvancedTriggerTapTargetCompleted, ) } } @@ -285,6 +318,9 @@ private fun TriggerScreenHorizontal( onMoveTriggerKey: (fromIndex: Int, toIndex: Int) -> Unit = { _, _ -> }, onFixErrorClick: (TriggerError) -> Unit = {}, onClickShortcut: (TriggerKeyShortcut) -> Unit = {}, + onRecordTriggerTapTargetCompleted: () -> Unit = {}, + onSkipTapTarget: () -> Unit = {}, + onAdvancedTriggerTapTargetCompleted: () -> Unit = {}, ) { Surface(modifier = modifier) { when (configState) { @@ -328,7 +364,11 @@ private fun TriggerScreenHorizontal( onRecordTriggerClick = onRecordTriggerClick, recordTriggerState = recordTriggerState, onAdvancedTriggersClick = onAdvancedTriggersClick, - showNewBadge = configState.showNewBadge, + showRecordTriggerTapTarget = (configState as? ConfigTriggerState.Empty)?.showRecordTriggerTapTarget + ?: false, + onRecordTriggerTapTargetCompleted = onRecordTriggerTapTargetCompleted, + onSkipTapTarget = onSkipTapTarget, + showAdvancedTriggerTapTarget = configState.showAdvancedTriggersTapTarget, ) } } @@ -368,6 +408,12 @@ private fun TriggerScreenHorizontal( ) } + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = stringResource(R.string.press_dot_dot_dot), + style = MaterialTheme.typography.labelLarge, + ) + if (configState.triggerModeButtonsVisible) { TriggerModeRadioGroup( modifier = Modifier.padding(horizontal = 8.dp), @@ -384,7 +430,11 @@ private fun TriggerScreenHorizontal( onRecordTriggerClick = onRecordTriggerClick, recordTriggerState = recordTriggerState, onAdvancedTriggersClick = onAdvancedTriggersClick, - showNewBadge = configState.showNewBadge, + showRecordTriggerTapTarget = false, + onRecordTriggerTapTargetCompleted = onRecordTriggerTapTargetCompleted, + onSkipTapTarget = onSkipTapTarget, + showAdvancedTriggerTapTarget = configState.showAdvancedTriggersTapTarget, + onAdvancedTriggerTapTargetCompleted = onAdvancedTriggerTapTargetCompleted, ) } } @@ -476,15 +526,17 @@ private fun ClickTypeRadioGroup( clickTypes: Set, checkedClickType: ClickType?, onSelectClickType: (ClickType) -> Unit, + maxLines: Int = 2, ) { Column(modifier = modifier) { - Row(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { if (clickTypes.contains(ClickType.SHORT_PRESS)) { RadioButtonText( modifier = Modifier.weight(1f), isSelected = checkedClickType == ClickType.SHORT_PRESS, text = stringResource(R.string.radio_button_short_press), onSelected = { onSelectClickType(ClickType.SHORT_PRESS) }, + maxLines = maxLines, ) } if (clickTypes.contains(ClickType.LONG_PRESS)) { @@ -493,6 +545,7 @@ private fun ClickTypeRadioGroup( isSelected = checkedClickType == ClickType.LONG_PRESS, text = stringResource(R.string.radio_button_long_press), onSelected = { onSelectClickType(ClickType.LONG_PRESS) }, + maxLines = maxLines, ) } if (clickTypes.contains(ClickType.DOUBLE_PRESS)) { @@ -501,6 +554,7 @@ private fun ClickTypeRadioGroup( isSelected = checkedClickType == ClickType.DOUBLE_PRESS, text = stringResource(R.string.radio_button_double_press), onSelected = { onSelectClickType(ClickType.DOUBLE_PRESS) }, + maxLines = maxLines, ) } } @@ -514,21 +568,17 @@ private fun TriggerModeRadioGroup( isEnabled: Boolean, onSelectParallelMode: () -> Unit, onSelectSequenceMode: () -> Unit, + maxLines: Int = 2, ) { Column(modifier = modifier) { - Text( - modifier = Modifier.padding(horizontal = 8.dp), - text = stringResource(R.string.press_dot_dot_dot), - style = MaterialTheme.typography.labelLarge, - ) - - Row(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { RadioButtonText( modifier = Modifier.weight(1f), isSelected = mode is TriggerMode.Parallel, isEnabled = isEnabled, text = stringResource(R.string.radio_button_parallel), onSelected = onSelectParallelMode, + maxLines = maxLines, ) RadioButtonText( modifier = Modifier.weight(1f), @@ -536,6 +586,7 @@ private fun TriggerModeRadioGroup( isEnabled = isEnabled, text = stringResource(R.string.radio_button_sequence), onSelected = onSelectSequenceMode, + maxLines = maxLines, ) } } @@ -586,7 +637,6 @@ private val previewState = ConfigTriggerState.Loaded( data = TriggerKeyShortcut.FINGERPRINT_GESTURE, ), ), - showNewBadge = true, ) @Preview(device = Devices.PIXEL) @@ -600,6 +650,17 @@ private fun VerticalPreview() { } } +@Preview(heightDp = 400, widthDp = 300) +@Composable +private fun VerticalPreviewTiny() { + KeyMapperTheme { + TriggerScreenVertical( + configState = previewState, + recordTriggerState = RecordTriggerState.Idle, + ) + } +} + @Preview(device = Devices.PIXEL) @Composable private fun VerticalEmptyPreview() { @@ -613,7 +674,6 @@ private fun VerticalEmptyPreview() { data = TriggerKeyShortcut.FINGERPRINT_GESTURE, ), ), - showNewBadge = true, ), recordTriggerState = RecordTriggerState.Idle, ) @@ -644,7 +704,6 @@ private fun HorizontalEmptyPreview() { data = TriggerKeyShortcut.FINGERPRINT_GESTURE, ), ), - showNewBadge = true, ), recordTriggerState = RecordTriggerState.Idle, diff --git a/app/src/main/java/io/github/sds100/keymapper/util/FilterUtils.kt b/app/src/main/java/io/github/sds100/keymapper/util/FilterUtils.kt index ea8a9073cc..c944f94985 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/FilterUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/FilterUtils.kt @@ -11,7 +11,7 @@ import java.util.Locale * Created by sds100 on 22/03/2021. */ -suspend fun List.filterByQuery(query: String?): Flow>> = flow { +fun List.filterByQuery(query: String?): Flow>> = flow { if (query.isNullOrBlank()) { emit(State.Data(this@filterByQuery)) } else { @@ -30,5 +30,5 @@ suspend fun List.filterByQuery(query: String?): Flow(val value: T, val children: MutableList> = mutableListOf()) + +inline fun TreeNode.breadFirstTraversal( + action: (T) -> Unit, +) { + val queue = ArrayDeque>() + queue.add(this) + + while (queue.isNotEmpty()) { + val currentNode = queue.removeFirst() + action(currentNode.value) + queue.addAll(currentNode.children) + } +} 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 de4cb1bea8..ec5d79d281 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 @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.util.ui import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.actions.pinchscreen.PinchPickCoordinateResult -import io.github.sds100.keymapper.actions.sound.ChooseSoundFileResult import io.github.sds100.keymapper.actions.swipescreen.SwipePickCoordinateResult import io.github.sds100.keymapper.actions.tapscreen.PickCoordinateResult import io.github.sds100.keymapper.constraints.Constraint @@ -37,6 +36,7 @@ sealed class NavDestination { const val ID_SHIZUKU_SETTINGS = "shizuku_settings" const val ID_CONFIG_FLOATING_BUTTON = "config_floating_button" const val ID_INTERACT_UI_ELEMENT_ACTION = "interact_ui_element_action" + const val ID_PRO_MODE = "pro_mode" } data class ChooseApp( @@ -80,7 +80,7 @@ sealed class NavDestination { override val id: String = ID_CHOOSE_ACTIVITY } - data object ChooseSound : NavDestination() { + data object ChooseSound : NavDestination() { override val id: String = ID_CHOOSE_SOUND } @@ -128,4 +128,8 @@ sealed class NavDestination { data class InteractUiElement(val action: ActionData.InteractUiElement?) : NavDestination() { override val id: String = ID_INTERACT_UI_ELEMENT_ACTION } + + data object ProMode : NavDestination() { + override val id: String = ID_PRO_MODE + } } 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 fc9c239439..ee8475853d 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 @@ -15,7 +15,6 @@ import io.github.sds100.keymapper.actions.keyevent.ConfigKeyEventActionFragment import io.github.sds100.keymapper.actions.pinchscreen.PinchPickCoordinateResult import io.github.sds100.keymapper.actions.pinchscreen.PinchPickDisplayCoordinateFragment import io.github.sds100.keymapper.actions.sound.ChooseSoundFileFragment -import io.github.sds100.keymapper.actions.sound.ChooseSoundFileResult import io.github.sds100.keymapper.actions.swipescreen.SwipePickCoordinateResult import io.github.sds100.keymapper.actions.swipescreen.SwipePickDisplayCoordinateFragment import io.github.sds100.keymapper.actions.tapscreen.PickCoordinateResult @@ -230,6 +229,8 @@ fun NavigationViewModel.setupNavigation(fragment: Fragment) { requestKey = requestKey, action = destination.action?.let { Json.encodeToString(destination.action) }, ) + + NavDestination.ProMode -> NavAppDirections.toProModeFragment() } fragment.findNavController().navigate(direction) @@ -306,7 +307,7 @@ fun NavigationViewModel.sendNavResultFromBundle( NavDestination.ID_CHOOSE_SOUND -> { val json = bundle.getString(ChooseSoundFileFragment.EXTRA_RESULT)!! - val result = Json.decodeFromString(json) + val result = Json.decodeFromString(json) onNavResult(NavResult(requestKey, result)) } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/CompactChip.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/CompactChip.kt index 2fe1b81ea9..e669f1733d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/CompactChip.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/CompactChip.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.github.sds100.keymapper.mappings.keymaps.chipHeight +import io.github.sds100.keymapper.keymaps.chipHeight @Composable fun CompactChip( diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/draggable/DragDropState.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DragDropState.kt similarity index 99% rename from app/src/main/java/io/github/sds100/keymapper/compose/draggable/DragDropState.kt rename to app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DragDropState.kt index 8d6d6a345a..2dfb597db2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/compose/draggable/DragDropState.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DragDropState.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.compose.draggable +package io.github.sds100.keymapper.util.ui.compose import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/draggable/DraggableItem.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DraggableItem.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/compose/draggable/DraggableItem.kt rename to app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DraggableItem.kt index 3bdf4f3fd4..b0653c9fb4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/compose/draggable/DraggableItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DraggableItem.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.compose.draggable +package io.github.sds100.keymapper.util.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperTapTarget.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperTapTarget.kt new file mode 100644 index 0000000000..71313f5f0b --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperTapTarget.kt @@ -0,0 +1,64 @@ +package io.github.sds100.keymapper.util.ui.compose + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.canopas.lib.showcase.component.ShowcaseStyle +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.onboarding.OnboardingTapTarget + +@Composable +fun KeyMapperTapTarget( + tapTarget: OnboardingTapTarget, + showSkipButton: Boolean = true, + onSkipClick: () -> Unit = {}, +) { + val textColor = MaterialTheme.colorScheme.onPrimary + Column { + Text( + text = stringResource(tapTarget.titleRes), + color = textColor, + style = MaterialTheme.typography.titleLarge, + ) + + Spacer(Modifier.height(16.dp)) + + Text( + text = stringResource(tapTarget.messageRes), + color = textColor, + style = MaterialTheme.typography.bodyMedium, + ) + + if (showSkipButton) { + Spacer(Modifier.height(16.dp)) + OutlinedButton( + onClick = onSkipClick, + border = BorderStroke( + width = 1.dp, + color = textColor, + ), + ) { + Text( + text = stringResource(R.string.tap_target_skip_tutorial_button), + color = textColor, + ) + } + } + } +} + +@Composable +fun keyMapperShowcaseStyle(): ShowcaseStyle { + return ShowcaseStyle( + backgroundColor = MaterialTheme.colorScheme.primary, + backgroundAlpha = 0.99f, + ) +} diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/RadioButtonText.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/RadioButtonText.kt index 16c7d38414..84f4b07702 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/RadioButtonText.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/RadioButtonText.kt @@ -21,13 +21,14 @@ fun RadioButtonText( text: String, isSelected: Boolean, isEnabled: Boolean = true, + maxLines: Int = 2, onSelected: () -> Unit, ) { Surface(modifier = modifier, shape = MaterialTheme.shapes.medium, color = Color.Transparent) { Row( modifier = Modifier .clickable(enabled = isEnabled, onClick = onSelected) - .padding(12.dp), + .padding(8.dp), verticalAlignment = Alignment.CenterVertically, ) { RadioButton( @@ -49,8 +50,8 @@ fun RadioButtonText( ), ) }, - maxLines = 2, - overflow = TextOverflow.Ellipsis, + maxLines = maxLines, + overflow = TextOverflow.Clip, ) } } diff --git a/app/src/main/res/layout/fragment_choose_sound_file.xml b/app/src/main/res/layout/fragment_choose_sound_file.xml index e71e4b9784..3da224818a 100644 --- a/app/src/main/res/layout/fragment_choose_sound_file.xml +++ b/app/src/main/res/layout/fragment_choose_sound_file.xml @@ -37,25 +37,41 @@ app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintTop_toBottomOf="@id/buttonChooseSystemRingtone" /> + android:layout_marginBottom="@dimen/bottom_app_bar_height" + app:layout_anchor="@+id/scrollView" + app:layout_anchorGravity="center"> @@ -121,25 +123,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="none" - android:text="@{viewModel.uiState.chosenDeviceName}" tools:ignore="LabelFor" /> - - + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_config_keymap.xml b/app/src/main/res/navigation/nav_config_keymap.xml index 824d90ac2c..4ea4136136 100644 --- a/app/src/main/res/navigation/nav_config_keymap.xml +++ b/app/src/main/res/navigation/nav_config_keymap.xml @@ -6,7 +6,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1d669e715e..ec3cc2f93a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,74 +1,1265 @@ + Tuşlarınızı özgür bırakın! + Key Mapper, uygulama dışında olduğunuzda düğme basışlarınızı algılayıp değiştirebilmesi için bir erişilebilirlik servisine ihtiyaç duyar. Tuş eşlemeleriniz, erişilebilirlik servisini etkinleştirdiğinizde çalışır. Tetikleyici oluşturmak ve eylemleri test etmek için de bu servisin açık olması gerekir. + seçildi + Etkinleştir + ¯\\_(ツ)_/¯\n\nBurada hiçbir şey yok! + İlk adım, tuş eşlemesini tetikleyecek bazı düğmeler eklemektir.\n\nÖnce ‘Tetikleyici Kaydet’ seçeneğine dokunun ve ardından yeniden eşlemek istediğiniz düğmelere basın. Bunlar burada görünecek.\n\nAlternatif olarak, bir tuş eşlemesini ‘gelişmiş tetikleyici’ kullanarak tetikleyebilirsiniz.\n\nİstediğiniz tuşları karıştırıp eşleştirebilirsiniz! + Root gerektirir + Bas… + Eylem yok + Tetikleyici yok + Bilinmeyen cihaz adı + Açık + Kapalı + Sistemi takip et + Bu cihaz + Herhangi bir cihaz + Varsayılan + Erişilebilirlik servisini etkinleştir + Erişilebilirlik servisini yeniden başlat + Paylaş + Burada hiçbir şey yok! + Key Mapper hiçbir etkileşim algılamadı. Ek öğeler göstermeyi deneyin. + Tekrarlamayı durdur… + Tetikleyici bırakıldığında + Tetikleyici tekrar basıldığında + Sınır ulaşıldığında + Tetikleyici bırakıldığında + Tetikleyici tekrar basıldığında + Gizli uygulamaları göster + Değiştiriciler + ÖNEMLİ!!! Bu koordinatlar yalnızca ekranınız ekran görüntüsüyle aynı yönde olduğunda doğrudur! Bu eylem, ekranda yaptığınız tüm dokunma veya hareketleri iptal eder.\n\nEkranınızdaki bir noktanın koordinatlarını bulmakta yardıma ihtiyacınız varsa, bir ekran görüntüsü alın ve ardından bu eylemin basmasını istediğiniz yere ekran görüntüsüne dokunun. + Not: \"İçeri sıkıştırma\" kullanırken X ve Y BİTİŞ koordinatlarıdır, \"Dışarı sıkıştırma\" kullanırken X ve Y BAŞLANGIÇ koordinatlarıdır. + Eylemleri düzeltmek için dokunun! + Kısıtlamaları düzeltmek için dokunun! + Eylemleri gerçekleştir + Tetikleyici şu olana kadar basılı tut… + Cihaz yok + ¯\\_(ツ)_/¯\n\nEkstra yok! + Tuş olayı eylemini yapılandırma tamamlandı + Koordinat seçimi tamamlandı + Key Mapper günlüğü + Neler Yeni + Sistem zil sesini kullanabilir veya özel bir ses dosyası seçebilirsiniz.\n\nÖzel ses dosyası, Key Mapper\'ın özel veri klasörüne kopyalanacak; bu, dosya taşınsa veya silinse bile işlemlerinizin çalışmaya devam edeceği anlamına gelir. Ayrıca, anahtar haritalarınızla birlikte zip klasöründe yedeklenecektir. +Kaydedilmiş ses dosyalarını ayarlardan silebilirsiniz. + Eşleştirilmiş cihaz bulunamadı. Bluetooth açık mı? + Kısayol olarak kullanmak için bir tuş eşlemesine dokunun. + Tuş eşleme kısayolu oluştur + Etkin + Devre dışı + Sıfırla + Cihaz yöneticisini etkinleştirdikten sonra, Key Mapper’ı kaldırmak istiyorsanız bunu DEVRE DIŞI BIRAKMANIZ gerekir. + %sms bekle + Etkinlik başlat: %s + Servis başlat: %s + Yayın gönder: %s + Tuş eşleme kimliği + Kabuğu kullan (Yalnızca ROOT) + Rahatsız Etmeyin modunda düzgün çalışması için izin gerekli! + Ekran kapalıyken tetikleme seçeneği, çalışması için root izni gerektirir! + Bu tetikleyici, telefon çalarken veya görüşme sırasında çalışmaz! + Android, telefonunuz çalarken veya bir görüşme sırasında erişilebilirlik servislerinin ses düğmesi basışlarını algılamasına izin vermez, ancak giriş yöntemi servisleri bunu algılayabilir. Bu nedenle, bu tetikleyicinin çalışmasını istiyorsanız Key Mapper klavyelerinden birini kullanmalısınız. + Android sınırlamaları nedeniyle harekette çok fazla parmak var. + Android sınırlamaları nedeniyle hareket süresi çok yüksek. + DPAD tetikleyicilerinin çalışması için bir Key Mapper klavyesi kullanmalısınız! + Tuş eşlemeleriniz rastgele duracak! + Tuş eşlemeleriniz duraklatıldı! + Devam ettir + Tuş eşlemelerinizin çalışması için erişilebilirlik servisinin açık olması gerekir! + Telefonunuz, Key Mapper’ı arka planda çalışırken kapattı veya çöktü! + Erişilebilirlik servisi etkin! Tuş eşlemeleriniz çalışmalı. + Ekstra günlük kaydı açık! Bir sorunu düzeltmeye çalışmıyorsanız bunu kapatın. + Kapat + Daha iyi ekran mesajları, daha fazla işlem ve hizmet güncellemeleri için bildirimleri açın. + Hakkında + %s uygulamasını aç + ‘%s’ yaz + %s%s gir + Kabuk üzerinden %s gir + %s cihazından %s%s gir + %s aç + Ekrana dokun (%d, %d) + Ekrana dokun (%s) + %d parmakla %d/%d koordinatlarından %d/%d koordinatlarına %dms içinde kaydır + %d parmakla %d/%d koordinatlarından %d/%d koordinatlarına %dms içinde kaydır (%s) + %s, %d parmakla %d/%d koordinatlarında %dpx sıkıştırma mesafesiyle %dms içinde + %s, %d parmakla %d/%d koordinatlarında %dpx sıkıştırma mesafesiyle %dms içinde (%s) + %s numarayı ara + Ses çal: %s + Bilinmeyen sesi oynat + Seçenekler: + Eylemler: + Tetikleyici: + Kısıtlamalar: + Yukarı kaydır + Aşağı kaydır + Sola kaydır + Sağa kaydır + Ekstralar + Başlangıç X + Başlangıç Y + Bitiş X + Bitiş Y + Sıkıştırma mesafesi (px) + Sıkıştırma türü + İçeri sıkıştırma + Dışarı sıkıştırma + Tuş kodu + Cihazdan + Kısayol adı + Koordinat açıklaması (isteğe bağlı) + Girilecek metin + Açılacak URL + Aranacak telefon numarası + Eylem + Kategoriler + Veri + Paket + Sınıf + Ad + Değer (%s) + Key Mapper için açıklama (gerekli) + Bayraklar + Ses dosyası açıklaması + WiFi ağ SSID’si + Aynı anda + Sırayla + VE + VEYA + Kısa basış + Uzun basış + Çift basış + Doğru + Yanlış + Etkinlik + Servis + Yayın alıcısı + Tetikleyici ve eylemler + Kısıtlamalar ve daha fazlası + Tetikleyici + Eylemler + Kısıtlamalar + Seçenekler + %s seçildi + Yedekleme başarılı! + Yedekleme başarısız! + Geri yükleme başarılı! + Geri yükleme başarısız! + Otomatik yedekleme başarılı! + Otomatik yedekleme başarısız! + Ekran görüntüsü alındı + Ekran görüntüsü çözünürlüğü bu cihazın çözünürlüğüyle eşleşmiyor! + Tuş eşleme UUID’si panoya kopyalandı + Bir tuş eşlemesini tetiklediniz + Günlük kopyalandı + Kaydedilmiş ses dosyanız yok! + Key Mapper, Shizuku kullanarak kendine WRITE_SECURE_SETTINGS izni verdi + Key Mapper, Root kullanarak kendine WRITE_SECURE_SETTINGS izni verdi + Sıra tetikleyici zaman aşımı + Uzun basış gecikmesi + Çift basış zaman aşımı + Tekrarlama gecikmesi + Tekrarlama sınırı + Her… tekrar et + Titreşim süresi + Kaç kez + Her tekrar için kaç kez + Sonraki eylemden önceki gecikme + Basılı tutma süresi + Kaydırma süresi (ms) + Parmak sayısı + Ekran görüntüsüyle ayarlanacak koordinatlar + Başlangıç + Bitiş + Sıkıştırma süresi (ms) + Parmak sayısı + %s ön planda + %s ön planda değil + %s medya oynatıyor + %s medya oynatmıyor + %s bağlı + %s bağlantısı kesildi + Ekran açık + Ekran kapalı + Flaş kapalı + Flaş açık + Ön flaş kapalı + Ön flaş açık + VE + VEYA + Ön plandaki uygulama + Ön planda olmayan uygulama + Bluetooth cihazı bağlı + Bluetooth cihazı bağlantısı kesildi + Ekran açık + Ekran kapalı + Dikey (0°) + Yatay (90°) + Dikey (180°) + Yatay (270°) + Dikey (herhangi) + Yatay (herhangi) + Medya oynatan uygulama + Medya oynatmayan uygulama + Medya oynuyor + Medya oynatmıyor + Flaş açık + Flaş kapalı + WiFi açık + WiFi kapalı + Bir WiFi ağına bağlı + Bir WiFi ağından bağlantı kesildi + Android 10 ve daha yeni sürümlerde uygulamaların bilinen WiFi ağlarının listesini sorgulamasına izin verilmez, bu nedenle SSID’yi manuel olarak yazmanız gerekecek. + + Herhangi bir WiFi ağının eşleşmesi gerekiyorsa boş bırakın. + Herhangi biri + %s WiFi’ye bağlı + %s WiFi’den bağlantı kesildi + Herhangi bir WiFi’ye bağlı + Hiçbir WiFi’ye bağlı değil + Giriş yöntemi seçildi + %s seçildi + Giriş yöntemi seçilmedi + %s seçilmedi + Cihaz kilitli + Cihaz kilidi açık + Kilit ekranı gösteriliyor + Kilit ekranı gösterilmiyor + Telefon görüşmesinde + Telefon görüşmesinde değil + Telefon çalıyor + Şarj oluyor + Şarj olmuyor + Dikey (0°) + Yatay (90°) + Dikey (180°) + Yatay (270°) + Zaman + %s ve %s arasındaki süre + Zaman sınırlaması + Başlangıç zamanı + Başlangıç zamanını düzenle + Bitiş zamanı + Bitiş zamanını düzenle + Uzun basış + Çift basış + Uygulama kapatma özelliğini kapat + Telefonunuzdaki tüm uygulama kapatma \"özelliklerini\" nasıl kapatacağınızı gösteren dontkillmyapp.com adresindeki harika kılavuzu takip edin. + + \n\nKılavuzu okuduktan sonra bir sonraki slayta geçmeniz ve erişilebilirlik servisini yeniden başlatmanız gerekecek. + Kılavuzu aç + Erişilebilirlik servisini yeniden başlat + Erişilebilirlik servisi yeniden başlatılmalıdır. Kapatıp açın. + Hata raporu oluştur + \"Rapor oluştur\" seçeneğine dokunarak hata raporunu kaydedeceğiniz bir konum seçin. Bir sonraki slayt, bunu geliştiriciye nasıl göndereceğinizi açıklayacak. + Rapor oluştur + Raporu paylaş + Hata raporunu geliştiriciyle paylaşmanın 2 yolu var. Discord sunucusuna katılabilir veya GitHub’da bir sorun oluşturabilirsiniz. Mesajınıza hata raporunu eklemeyi unutmayın! + Discord + GitHub + Ayarlar + Hakkında + Ara + Yardım + Hata bildir + Giriş yöntemi seçiciyi göster + Kaydet + Geri yükle + Her şeyi yedekle + Dokunarak duraklat + Dokunarak devam ettir + Kaydet + Kısa mesajları aç/kapat + Kopyala + Temizle + Eylem ekle + Tetikleyici kaydet + Gelişmiş tetikleyiciler + YENİ! + Tamam + Düzelt + Kaydediyor (%d…) + Kısıtlama ekle + Tuş kodu seç + Ekstra ekle + Başlatıcıda kısayol oluştur + Kısayolu manuel olarak oluştur + Intent kılavuzu + Yardım + Ekran görüntüsü seç (isteğe bağlı) + Etkinlik seç + Bayrakları ayarla + Sınır yok + Ses dosyası seç + Sistem zil sesini seç + Eylemi düzenle + Eylemi değiştir + Root izni gerekli! + Erişilebilirlik ayarları sayfası bulunamadı + Kaydedilmemiş değişiklikler + Kaydedilmemiş değişiklikleriniz var. Bunları iptal ederseniz, düzenlemeleriniz kaybolacak. + Telefonunuzun rootlu olmadığını biliyorsanız veya rootun ne olduğunu bilmiyorsanız, yalnızca rootlu cihazlarda çalışan özellikleri kullanamazsınız. ‘Tamam’a dokunduğunuzda ayarlara yönlendirileceksiniz. + Ayarlarda, en alta kaydırın ve root özelliklerini/eylemlerini kullanabilmek için ‘Key Mapper root iznine sahip’ seçeneğine dokunun. + WRITE_SECURE_SETTINGS izni ver + Bu izni vermek için bir PC/Mac gereklidir. Çevrimiçi kılavuzu okuyun. + Cihazınızda erişilebilirlik servisleri ayarları sayfası yok gibi görünüyor. Bunu nasıl düzelteceğinizi açıklayan çevrimiçi kılavuzu okumak için “kılavuz” seçeneğine dokunun. + Tuşlar, basılı tutulacakları sırayla yukarıdan aşağıya listelenmelidir. + Bir “sıra” tetikleyicisi, paralel tetikleyicilerden farklı olarak bir zaman aşımına sahiptir. Bu, ilk tuşa bastıktan sonra tetikleyicideki diğer tuşları girmeniz için belirli bir süreniz olacağı anlamına gelir. Tetikleyiciye eklediğiniz tüm tuşlar, zaman aşımı süresine ulaşılana kadar normal işlevlerini yerine getirmez. Bu zaman aşımını “Seçenekler” sekmesinden değiştirebilirsiniz. + Android, uygulamaların bağlı (eşleştirilmemiş) Bluetooth cihazlarının listesini almasına izin vermez. Uygulamalar yalnızca bu cihazların bağlandığını ve bağlantısının kesildiğini algılayabilir. Bu nedenle, Bluetooth cihazınız erişilebilirlik servisi başladığında zaten bağlıysa, uygulamanın bunu bilmesi için cihazı yeniden bağlamanız gerekecek. + Konumu değiştir veya otomatik yedeklemeyi kapat? + Ekran açma/kapama kısıtlamaları, yalnızca “ekran kapalıyken tetikleyiciyi algıla” tuş eşleme seçeneğini açtıysanız çalışır. Bu seçenek, bazı tuşlar (örneğin ses düğmeleri) için ve yalnızca rootluysanız görünür. Desteklenen tuşların listesini Yardım sayfasında görebilirsiniz. + PIN veya Desen gibi başka bir ekran kilidiniz varsa endişelenmenize gerek yok. Ancak yalnızca Parola ekran kilidi kullanıyorsanız, Key Mapper Temel Giriş Yöntemi’ni kullanırsanız telefonunuzun kilidini açamazsınız çünkü bu yöntemin bir arayüzü yoktur. Key Mapper’a WRITE_SECURE_SETTINGS izni vererek klavyeyi değiştirmek için bir bildirim gösterebilirsiniz. Bunu nasıl yapacağınızı öğrenmek için ekranın altındaki soru işaretine dokunun. + Eylemler için bir giriş yöntemi gerektiren bir yöntem seçin. Bunu daha sonra ana ekranın alt menüsündeki “Eylemler için klavye seç” seçeneğiyle değiştirebilirsiniz. + Caps Lock tuşunun hala büyük harf kilitlemesini engellemek için klavyenizde “Caps Lock’u kameraya” klavye düzenini seçmeniz gerekir. Bu ayarı cihaz ayarlarınızda -> Diller ve Giriş -> Fiziksel Klavye -> Klavyenize dokunun -> Klavye Düzenlerini Ayarla bölümünden bulabilirsiniz. Bu, Caps Lock tuşunu KEYCODE_CAMERA’ya yeniden eşleyerek Key Mapper’ın bunu doğru şekilde eşlemesini sağlar.\n\nBunu yaptıktan sonra Caps Lock tetikleyici tuşunu kaldırıp Caps Lock tuşunu tekrar kaydetmelisiniz. Adımları doğru yaptıysanız “Caps Lock” yerine “Kamera” yazmalıdır. + Bağlı harici cihaz yok. + Key Mapper GUI Klavyesini Yükle + Bu şiddetle tavsiye edilir! Bu, Key Mapper ile kullanabileceğiniz uygun bir klavyedir. Key Mapper’a dahili olan (Temel Giriş Yöntemi) klavyede ekran klavyesi yoktur. Nereden yüklemek istediğinizi seçin. + Key Mapper Leanback Klavyesini Yükle + Bu şiddetle tavsiye edilir! Bu, Key Mapper ile kullanabileceğiniz Android TV için uygun bir klavyedir. Key Mapper’a dahili olan (Temel Giriş Yöntemi) klavyede ekran klavyesi yoktur. Nereden yüklemek istediğinizi seçin. + Key Mapper GUI Klavyesini Yükle + Nereden indirmek istediğinizi seçin. + Key Mapper Leanback Klavyesini Yükle + Nereden indirmek istediğinizi seçin. + Bu eylem için ek kurulum gerekiyor + Bu eylemi kullanmak için cihazınızı ayarlamanın 3 yolu var. Her birinin avantajları ve dezavantajları şunlardır: + + \n\n1. Shizuku’yu indirin (önerilen). Şu anda kullandığınız ekran klavyesini değiştirmeniz gerekmez, ancak cihazınızı her yeniden başlattığınızda bir dakikalık kurulum gerektirir. + + \n\n2. Key Mapper GUI Klavyesini indirin. Bu, Key Mapper ile kullanabileceğiniz bir ekran klavyesidir, ancak şu anda kullandığınız klavyeyi (örneğin Gboard) kullanamazsınız. + + \n\n3. Hiçbir şey yapmayın ve dahili Key Mapper klavyesini kullanın. Bu önerilmez çünkü Key Mapper’ı kullandığınızda hiçbir ekran klavyeniz olmaz! Hiçbir avantajı yoktur. + Bu eylem için ek kurulum gerekiyor + Bu eylemi kullanmak için cihazınızı ayarlamanın 3 yolu var. Her birinin avantajları ve dezavantajları şunlardır: + + \n\n1. Shizuku’yu indirin (önerilen). Şu anda kullandığınız ekran klavyesini değiştirmeniz gerekmez, ancak cihazınızı her yeniden başlattığınızda bir dakikalık kurulum gerektirir. + + \n\n2. Key Mapper Leanback Klavyesini indirin. Bu, Key Mapper ile kullanabileceğiniz Android TV için optimize edilmiş bir ekran klavyesidir, ancak şu anda kullandığınız klavyeyi (örneğin Gboard) kullanamazsınız. + + \n\n3. Hiçbir şey yapmayın ve dahili Key Mapper klavyesini kullanın. Bu önerilmez çünkü Key Mapper’ı kullandığınızda hiçbir ekran klavyeniz olmaz! Hiçbir avantajı yoktur. + Pil optimizasyonunu devre dışı bırak + HEPSİNİ okumanız ZORUNLU, aksi takdirde ileride sinir bozucu sorunlar yaşarsınız!\n\n“Kısmen düzelt” seçeneğine dokunmak, Android’in uygulamayı arka planda durdurmasını belki engelleyebilir.\n\nBu YETERLİ DEĞİL. MIUI veya Samsung Experience gibi OEM arayüzünüzde başka uygulama kapatma özellikleri olabilir, bu nedenle dontkillmyapp.com’daki çevrimiçi kılavuzu takip ederek Key Mapper için bunları da kapatmalısınız. + Erişilebilirlik servisini kapatıp açarak yeniden başlatın. + Bu tetikleyiciyi kullanmak, cihazınızın ayarlarındaki ekran sabitleme ayarını kullandıktan sonra cihazınızın kilidini açtığınızda siyah bir ekran oluşmasına neden olabilir. Bu, bir yeniden başlatma ile düzeltilebilir. Bu durum tüm cihazlarda olmaz, bu yüzden dikkatli olun ve sorun yaşarsanız ayarı kapatın! + Key Mapper kesintiye uğradı + Key Mapper arka planda çalışmaya çalıştı ancak sistem tarafından durduruldu.\nBu, pil veya bellek optimizasyonu açık olduğunda olabilir.\n\nBunu düzeltmek için çevrimiçi bir kılavuzu takip edebilirsiniz. İşiniz bittiğinde servisi de yeniden başlatmalısınız. + Devam et + Yoksay + Hata raporu oluşturulamadı + Hatayı düzelt + Key Mapper için dosya oluşturmanıza izin veren bir dosya uygulamanız yüklü değil. Lütfen bir dosya yöneticisi yükleyin. + Key Mapper için dosya seçmenize izin veren bir dosya uygulamanız yüklü değil. Lütfen bir dosya yöneticisi yükleyin. + Erişilebilirlik servisi etkinleştirilmeli + @string/accessibility_service_explanation + Rahatsız Etmeyin erişimini ver + Cihazınızın hangi uygulamaların Rahatsız Etmeyin durumunu değiştirebileceğini yönetebileceğiniz ayarlar sayfasına yönlendirileceksiniz. Bu bazı cihazlarda mevcut değildir, bu yüzden listede Key Mapper’ı görmüyorsanız “tekrar gösterme” seçeneğine dokunun. + Bilmenizde fayda var! + Bir tetikleyici tuşunun yanında bu sembolü (⌨) görüyorsanız, algılanması için bir Key Mapper klavyesi KULLANMALISINIZ. Bu, Android’deki bir kısıtlamadır ve yalnızca bazı düğmeler için gereklidir. + Önemli! + Key Mapper GUI Klavyesini, bu Key Mapper sürümüyle uyumlu olacak şekilde güncellemelisiniz. Güncelleme yapana kadar bazı tuş eşlemeleri çalışmayabilir! + Şimdi güncelle + Yoksay + Şuna göre sırala + Öncelikleri ayarlamak için tutamaçları sürükleyin. En üstteki öğe en önemlisidir. Ayrıca herhangi bir öğeye dokunarak sıralama sırasını tersine çevirebilirsiniz. + Örnek: Tuş eşlemelerini öncelikle Eylemlerine göre artan sırayla ve ikincil olarak Tetikleyicilerine göre azalan sırayla sıralamak için Eylemleri birinci sıraya, Tetikleyicileri ikinci sıraya taşıyın. + %1$s için tutamaç + Örnek göster + Tanınmayan tuş kodu + Basılı düğme, giriş sistemi tarafından tanınmadı. Geçmişte Key Mapper bu tür düğmeleri tek bir düğme olarak algılıyordu. Şu anda uygulama, düğmeyi tarama koduna göre ayırt etmeye çalışıyor; bu, daha benzersiz olmalıdır. Ancak bu, benzersizliği garanti etmeyen geçici ve eksik bir çözümdür. + Tamam + Kılavuz + Kılavuz + Değiştir + Kısmen düzelt + Tamam + Yeniden başlat + Tekrar gösterme + Uygula + Değişiklikleri iptal et + Kaydet + Anladım + Kapat + İptal + Tekrar gösterme + Düzenlemeye devam et + Gizle + Çevrimiçi kılavuz + Ayarlar + Belgeler + Değişiklik günlüğü + Shizuku + Key Mapper GUI Klavyesi + Key Mapper Leanback Klavyesi + Hiçbir şey yapma + Düzelt + Klavye seçici + Tuş eşlemelerini duraklat/devam ettir + Klavye gizli uyarısı + Key Mapper klavyesini aç/kapat + Yeni özellikler + Klavyenizi değiştirmek için dokunun. + Klavye seçici + Çalışıyor + Key Mapper’ı açmak için dokunun. + Duraklat + Duraklatıldı + Key Mapper’ı açmak için dokunun. + Devam ettir + Kapat + Yeniden başlat + Erişilebilirlik servisi devre dışı + Erişilebilirlik servisini başlatmak için dokunun. + Erişilebilirlik servisi yeniden başlatılmalı! + Erişilebilirlik servisi çöktü! Telefonunuz bunu agresif bir şekilde kapatmış olabilir! Erişilebilirlik servisini yeniden başlatmak için dokunun. + Servisi durdur + Klavye gizli! + Klavyeyi tekrar göstermeye başlamak için ‘klavyeyi göster’e dokunun. + Key Mapper klavyesini aç/kapat + Key Mapper klavyesine ve klavyenizden geçiş yapmak için ‘aç/kapat’a dokunun. + Aç/Kapat + Varsayılan uzun basış gecikmesi (ms) + Bir düğmenin uzun basış olarak algılanması için ne kadar süre basılı tutulması gerektiği. Varsayılan 500ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan çift basış süresi (ms) + Bir düğmenin çift basış olarak algılanması için ne kadar hızlı çift basılması gerektiği. Varsayılan 300ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Bir tuş eşlemesi için titreşim etkinse ne kadar süre titreşeceği. Varsayılan 200ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan titreşim süresi (ms) + Eylemin tekrarlanmaya başlaması için tetikleyicinin ne kadar süre basılı tutulması gerektiği. Varsayılan 400ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan tekrar gecikmesi (ms) + Bir eylemin her tekrar arasındaki gecikme. Varsayılan 50ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan tekrarlar arası gecikme (ms) + Bir sıra tetikleyicisini tamamlamak için izin verilen süre. Varsayılan 1000ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan sıra tetikleyici zaman aşımı (ms) + Sıfırla + Tüm tuş eşlemelerini titreşime zorla. + Titreşimi zorla + Klavye seçici bildirimi + Klavye seçmenize olanak tanıyan kalıcı bir bildirim göster. + Tuş eşlemelerini duraklat/devam ettir bildirimi + Tuş eşlemelerinizi başlatan/duraklatan kalıcı bir bildirim göster. + Tuş eşlemelerini belirtilen bir konuma otomatik olarak yedekle + Konum seçilmedi. + Cihazları seç + Klavye seçiciyi otomatik olarak göster + Seçtiğiniz bir cihaz bağlandığında veya bağlantısı kesildiğinde klavye seçici otomatik olarak gösterilir. Aşağıdan cihazları seçin. + Bir cihaz (örneğin klavye) bağlandığında/bağlantısı kesildiğinde ekran klavyesini otomatik olarak değiştir + Seçilen bir cihaz bağlandığında son kullanılan Key Mapper klavyesi otomatik olarak seçilir. Cihazın bağlantısı kesildiğinde normal klavyeniz otomatik olarak seçilir. + Metin girmeye başladığınızda ekran klavyesini otomatik olarak değiştir + Klavyeyi açmaya çalıştığınızda son kullanılan Key Mapper dışı klavye otomatik olarak seçilir. Klavyeyi kullanmayı bıraktığınızda Key Mapper klavyeniz otomatik olarak seçilir. + Klavyeyi otomatik olarak değiştirirken ekranda bir mesaj göster + Key Mapper root iznine sahip + Yalnızca rootlu cihazlarda çalışan özellikleri/eylemleri kullanmak istiyorsanız bunu etkinleştirin. Bu özelliklerin çalışması için Key Mapper’ın root erişim yönetim uygulamanızdan (örneğin Magisk, SuperSU) root izni almış olması gerekir. + Bunu yalnızca cihazınızın rootlu olduğunu biliyor ve Key Mapper’a root izni verdiyseniz açın. + Tema seç + Açık ve koyu temalar mevcut + Bildirime dokunduğunuzda Key Mapper klavyesi ile varsayılan klavyeniz arasında geçiş yapın. + Key Mapper klavyesini aç/kapat bildirimi + Tuş eşlemelerini açarken/kapatırken klavyeyi otomatik olarak değiştir + Tuş eşlemelerinizi devam ettirdiğinizde Key Mapper klavyesini, duraklattığınızda ise varsayılan klavyenizi otomatik olarak seçin. + Ana ekran uyarılarını gizle + Ana ekranın üstündeki uyarıları gizle. + Cihaza özgü tetikleyiciler için cihaz kimliğinin ilk 5 karakterini göster + Bu, aynı ada sahip cihazları ayırt etmek için kullanışlıdır. + ABD İngilizcesine ayarlanmış klavyeleri düzelt + Bu, bir erişilebilirlik servisi etkinleştirildiğinde doğru klavye düzenine sahip olmayan klavyeleri düzeltir. Daha fazla bilgi okuyup yapılandırmak için dokunun. + ABD İngilizcesine ayarlanmış klavyeleri düzelt + Android 11’de bir hata var; bir erişilebilirlik servisi açıldığında Android, tüm harici cihazları aynı dahili sanal cihaz olarak görüyor. Bu cihazları doğru şekilde tanımlayamadığı için hangi klavye düzenini kullanacağını bilemiyor ve örneğin bir Alman klavyesi olsa bile varsayılan olarak ABD İngilizcesini kullanıyor. Aşağıdaki adımları izleyerek Key Mapper ile bu sorunu çözebilirsiniz. + 4. Cihazları seç + 1. Key Mapper GUI Klavyesini yükle (isteğe bağlı) + 1. Key Mapper Leanback Klavyesini yükle (isteğe bağlı) + 2. Key Mapper GUI Klavyesini veya Key Mapper Temel Giriş Yöntemi’ni etkinleştir + 2. Key Mapper Leanback Klavyesini veya Key Mapper Temel Giriş Yöntemi’ni etkinleştir + 3. Az önce etkinleştirdiğiniz klavyeyi kullan + (Önerilen) Bu ayar için kullanıcı kılavuzunu okuyun. + Ekstra günlüğü etkinleştir + Günlüğü görüntüle ve paylaş + Sorun bildir + Ses dosyalarını sil + Ses eylemi için kullanılabilecek ses dosyalarını sil. + İzin ver + İzin verildi + 1. Shizuku yüklü değil! Shizuku uygulamasını indirmek için dokunun. + 1. Shizuku yüklü. + 2. Shizuku başlatılmadı! Shizuku uygulamasını açmak için dokunun ve ardından nasıl başlatılacağına dair talimatlarını okuyun. + 2. Shizuku başlatıldı. + 3. Key Mapper’ın Shizuku’yu kullanma izni yok. Bu izni vermek için dokunun. + 3. Key Mapper otomatik olarak Shizuku’yu kullanacak. Key Mapper’ın hangi özelliklerinin Shizuku’yu kullandığını okumak için dokunun. + Varsayılan eşleme seçenekleri + Tuş eşlemeleriniz için varsayılan seçenekleri değiştirin. + Tüm ayarları sıfırla + TEHLİKE! Uygulamadaki tüm ayarları varsayılana sıfırla. Tuş eşlemeleriniz sıfırlanMAYACAK. + TEHLİKE! + Uygulamadaki tüm ayarları varsayılana sıfırlamak istediğinizden emin misiniz? Tuş eşlemeleriniz sıfırlanMAYACAK. Giriş ekranı ve tüm uyarı pop-up’ları tekrar görünecek. + Evet, sıfırla + Klavye seçiciyi otomatik olarak göster + Klavye seçiciyi otomatik olarak göstermeye olanak tanıyan ayarları görmek için dokunun. + Root ayarları + Bu seçenekler yalnızca rootlu cihazlarda çalışır! Rootun ne olduğunu veya cihazınızın rootlu olup olmadığını bilmiyorsanız, bunlar çalışmazsa lütfen kötü bir inceleme bırakmayın. :) + WRITE_SECURE_SETTINGS izni gerektirir + Bu seçenekler yalnızca Key Mapper WRITE_SECURE_SETTINGS iznine sahipse etkinleşir. İzni nasıl vereceğinizi öğrenmek için aşağıdaki düğmeye tıklayın. + Shizuku desteği + Shizuku, Key Mapper’ın yalnızca sistem uygulamalarının yapabileceği şeyleri yapmasını sağlayan bir uygulamadır. Örneğin, Key Mapper klavyesini kullanmanız gerekmez. Bunu nasıl kuracağınızı öğrenmek için dokunun. + Shizuku’yu kurmak için bu adımları izleyin. + Klavyeyi otomatik olarak değiştir + Bunlar gerçekten kullanışlı ayarlar ve kontrol etmeniz önerilir! + Günlük kaydı + Bu, tuş eşlemelerinize gecikme ekleyebilir, bu yüzden yalnızca uygulamayı hata ayıklamaya çalışıyorsanız veya geliştirici tarafından istenmişse açın. + Ses diyaloğunu göster + Titre + Ekranda mesaj göster + Uzun basışta tekrar titre + Ekran kapalıyken tetikleyiciyi algıla + Tekrarla + %dx + %dms sonra + her %dms’de + tekrar basılana kadar + bırakılana kadar + Tekrarla + Basılı tut + Tekrar basılana kadar basılı tut + Yeniden eşleme yapma + Bu tuş eşlemesini diğer uygulamaların tetiklemesine izin ver + Tuş eşleme kimliğini kopyala + + Erişilebilirlik + Alarm + DTMF + Müzik + Bildirimler + Zil + Sistem + Sesli arama + Normal + Titreşim + Sessiz + Ön + Arka + Alarmlar + Öncelik + Hiçbir şey + Tuş eşlemelerini duraklat + Tuş eşlemelerini devam ettir + Duraklatıldı + Çalışıyor + Servis Devre Dışı + Key Mapper erişilebilirlik servisi devre dışı + Key Mapper klavyesini aç/kapat + Ctrl + Sol Ctrl + Sağ Ctrl + Alt + Sol Alt + Sağ Alt + Shift + Sol Shift + Sağ Shift + Meta + Sol Meta + Sağ Meta + Sym + Func + Caps Lock + Num Lock + Scroll Lock + Bu eylemin çalışması için Key Mapper klavyelerinden birini kullanıyor olmanız gerekiyor! + %s paket adına sahip uygulama yüklü değil! + %s uygulaması devre dışı! + Key Mapper\'a sistem ayarlarını değiştirme izni vermeniz gerekiyor. + Bu işlem root izni gerektiriyor! + Bu eylem kamera izni gerektiriyor! + Android %s veya daha yeni bir sürüm gerektiriyor + Android %s veya daha eski bir sürüm gerektiriyor + Cihazınızda kamera bulunmuyor. + Cihazınız NFC\'yi desteklemiyor. + Cihazınızda parmak izi okuyucu bulunmuyor. + Cihazınız WiFi\'yi desteklemiyor. + Cihazınız Bluetooth\'u desteklemiyor. + Cihazınız cihaz politikası uygulamasını desteklemiyor. + Cihazınızda kamera flaşı bulunmuyor. + Cihazınızda telefon özellikleri bulunmuyor. + Klavye ayarları sayfası bulunamadı! + Key Mapper\'ın cihaz yöneticisi olması gerekiyor! + Key Mapper\'ın bu kısayolu kullanma izni yok + Uygulamanın Rahatsız Etmeyin durumunu değiştirmek için izne ihtiyacı var! + Bu eylem telefon durumunu okuma izni gerektiriyor! + WRITE_SETTINGS izin sayfası bulunamadı! + Bu uygulama kısayolu açılırken hata oluştu + Rahatsız Etmeyin erişim izni ayarları bulunamadı! + Key Mapper\'ın WRITE_SECURE_SETTINGS iznine ihtiyacı var. + Bu telefon aramasını başlatabilecek bir uygulama yok + Kamera kullanımda! + Kamera bağlantısı kesildi! + Kamera devre dışı! + Kamera hatası! + Maksimum kamera kullanımda! + Ön flaş yok + Arka flaş yok + Değişken flaş ışığı gücü desteklenmiyor + Erişilebilirlik servisinin etkinleştirilmesi gerekiyor! + Erişilebilirlik servisinin yeniden başlatılması gerekiyor! + Başlatıcınız kısayolları desteklemiyor. + Bir Key Mapper klavyesinin etkinleştirilmesi gerekiyor! + %s giriş yöntemi bulunamadı + Giriş yöntemi seçici gösterilemiyor! + Erişilebilirlik düğümü bulunamadı! + %s genel eylemi gerçekleştirilemedi! + Pil optimizasyon ayarları bulunamadı! Varsa, manuel olarak açın. + Ekstra (%s) bulunamadı! + Aynı kısıtlamaya iki kez sahip olamazsınız! + Boş olamaz! + Cihaz bulunamadı! + Boş JSON dosyası! + Dosya erişimi reddedildi! %s + Bilinmeyen G/Ç hatası! + İptal edildi! + Geçersiz numara! + En az %s olmalı! + En fazla %s olmalı! + Pil optimizasyonu açık! Key Mapper\'ın rastgele durmasını engellemek için bunu kapatın. + Bildirim erişim izni reddedildi! + Geçersiz! + Telefon araması başlatma izni reddedildi! + Bu yedeği kullanmak için Key Mapper\'ı en son sürüme güncellemeniz gerekiyor. + Sesli asistan yüklü değil! + Yetersiz izinler + Yalnızca Key Mapper klavyeleri yüklü! + Medya oynatan bir uygulama yok! + Kaynak dosya bulunamadı! %s + Hedef dosya bulunamadı! %s + Hareket girişi başarısız! + Sistem ayarı %s değiştirilemedi! + %s etkinleştirilmeli! + Giriş yöntemi değiştirilemedi! + Cihazınızda kamera uygulaması yok! + Cihazınızda asistan yok! + Cihazınızda ayarlar uygulaması yok! + Bu URL\'yi açabilecek bir uygulama yok! + Klasör değil! %s + Dosya değil! %s + Dizin bulunamadı! %s + Ses dosyası bulunamadı! + Depolama izni reddedildi! + Kaynak ve hedef aynı olamaz! + Hedefte yer kalmadı! %s + Shizuku izni reddedildi! + Shizuku başlatılmadı! + Bu dosyanın adı yok! + Geçersiz dosya. Key Mapper\'dan dışa aktarılmış bir zip olmalı. + Key Mapper\'a eşleştirilmiş Bluetooth cihazlarını görme izni vermelisiniz. + Hatalı URL. http:// kısmını unuttunuz mu? + Hassas konum okuma izni reddedildi! + Telefon aramalarını yanıtlama ve sonlandırma izni reddedildi! + Eşleştirilmiş Bluetooth cihazlarını görme izni reddedildi! + Bildirim gösterme izni reddedildi! + 2 veya daha fazla olmalı! + %d veya daha az olmalı! + 0\'dan büyük olmalı! + 0\'dan büyük olmalı! + 0\'dan büyük olmalı! + 0\'dan büyük olmalı! + %d veya daha az olmalı! + UI öğesi bulunamadı! + WiFi\'yi aç/kapat + WiFi\'yi aç + WiFi\'yi kapat + Bluetooth\'u aç/kapat + Bluetooth\'u aç + Bluetooth\'u kapat + Sesi artır + Sesi azalt + Sesi kapat + Sesi aç/kapat + Sesi aç + Ses kontrol panelini göster + Akışı artır + %s akışını artır + Akışı azalt + %s akışını azalt + Zil modlarını değiştir (Normal, Titreşim, Sessiz) + Zil modlarını değiştir (Normal, Titreşim) + Zil modunu değiştir + %s moduna geç + Rahatsız Etmeyin modunu aç/kapat + Yalnızca %s için Rahatsız Etmeyin modunu aç/kapat + Rahatsız Etmeyin modunu aç + Yalnızca %s için Rahatsız Etmeyin modunu aç + Rahatsız Etmeyin modunu kapat + Otomatik döndürmeyi aç + Otomatik döndürmeyi kapat + Otomatik döndürmeyi aç/kapat + Dikey mod + Yatay mod + Yönü değiştir + Dönüşleri sırayla değiştir + %s dönüşlerini sırayla değiştir + Mobil veriyi aç/kapat + Mobil veriyi aç + Mobil veriyi kapat + Otomatik parlaklığı aç/kapat + Otomatik parlaklığı kapat + Otomatik parlaklığı aç + Ekran parlaklığını artır + Ekran parlaklığını azalt + Bildirim panelini aç + Bildirim panelini aç/kapat + Hızlı ayarları aç + Hızlı ayarlar panelini aç/kapat + Durum çubuğunu kapat + Medya oynatmayı duraklat + Bir uygulama için medya oynatmayı duraklat + %s için medyayı duraklat + Medya oynatmayı devam ettir + Bir uygulama için medya oynatmayı devam ettir + %s için medyayı devam ettir + Medya oynatmayı oynat/duraklat + Bir uygulama için medya oynatmayı oynat/duraklat + %s için medyayı oynat/duraklat + Sonraki parça + Bir uygulama için sonraki parça + %s için sonraki parça + Önceki parça + Bir uygulama için önceki parça + %s için önceki parça + Hızlı ileri sar + Bir uygulama için hızlı ileri sar + %s için hızlı ileri sar + Tüm medya uygulamaları hızlı ileri sarmayı desteklemez. Örn. Google Play Music. + Geri sar + Bir uygulama için geri sar + %s için geri sar + Tüm medya uygulamaları geri sarmayı desteklemez. Örn. Google Play Music. + Medyayı durdur + Bir uygulama için medyayı durdur + %s için medyayı durdur + Medyayı ileri sar + Bir uygulama için medyayı ileri sar + %s için medyayı ileri sar + Medyayı geri sar + Bir uygulama için medyayı geri sar + %s için medyayı geri sar + Geri dön + Ana ekrana git + Son uygulamaları aç + Menüyü aç + Bölünmüş ekranı aç/kapat + Son uygulamaya git (Son uygulamalara çift bas) + Flaş ışığını aç/kapat + Flaş ışığını aç + Flaş ışığını kapat + Flaş ışığını aç/kapat + Flaş ışığını aç/kapat (%s) + Flaş ışığını aç + Flaş ışığını aç (%s) + Flaş ışığını kapat + Flaş ışığı parlaklığını değiştir + Flaş ışığını %s artır + Flaş ışığını %s azalt + Ön flaş ışığını aç/kapat + Ön flaş ışığını aç/kapat (%s) + Ön flaş ışığını aç + Ön flaş ışığını aç (%s) + Ön flaş ışığını kapat + Ön flaş ışığı parlaklığını değiştir + Ön flaş ışığını %s artır + Ön flaş ışığını %s azalt + NFC\'yi aç + NFC\'yi kapat + NFC\'yi aç/kapat + Ekran görüntüsü al + Sesli asistanı başlat + Cihaz asistanını başlat + Kamerayı aç + Cihazı kilitle + Cihazı güvenli bir şekilde kilitle + Tekrar giriş yapmak için yalnızca PIN\'inizi kullanabilirsiniz. Parmak izi tarayıcı ve yüz tanıma devre dışı bırakılacaktır. Bu, Android Pie 9.0 öncesi root olmayan cihazları kilitlemenin bulduğum tek güvenilir yoludur. + Cihazı uyut/uyandır + Ekran kapalıyken tetikleyiciyi algılama seçeneğini açmanız gerekiyor! + Hiçbir şey yapma + İmleci sona taşı + Bu eylem bazı uygulamalarda beklendiği gibi çalışmayabilir. + Klavyeyi aç/kapat + Bu eylem yalnızca klavyenin gösterilmesi gereken bir giriş alanına dokunduğunuzda çalışır. + Klavyeyi göster + Klavyeyi gizle + Klavye seçiciyi göster + Klavyeyi değiştir + %s klavyesine geç + Kes + Kopyala + Yapıştır + İmlecin olduğu kelimeyi seç + Ayarları aç + Güç menüsünü göster + Uçak modunu aç/kapat + Uçak modunu aç + Uçak modunu devre dışı bırak + Uygulamayı başlat + Bazı cihazlar, uygulamaların arka planda başka uygulamaları başlatabilmesi için izne ihtiyaç duyar. Web sitemizdeki talimatları görmek için \"Daha fazla oku\"ya dokunun. + Daha fazla oku + Yoksay + Uyarı! + Uygulama kısayolunu başlat + Tuş kodunu gir + Tuş olayını gir + Ekrana dokun + Ekranı kaydır + Ekranı sıkıştır + Metin gir + URL aç + Intent gönder + Telefon araması başlat + Telefon aramasını cevapla + Telefon aramasını sonlandır + Ses çal + En son bildirimi kaldır + Tüm bildirimleri kaldır + Cihaz kontrol ekranı + HTTP isteği + HTTP Yöntemi + Açıklama + Boş bırakılamaz! + URL + Boş bırakılamaz! + Hatalı URL. http:// kısmını unuttunuz mu? + İstek gövdesi (opsiyonel) + Yetkilendirme başlığı (opsiyonel) + Gerekirse \'Bearer\' ön ekini kullanın + Uygulama öğesiyle etkileşime geç + Key Mapper, menüler, sekmeler, düğmeler ve onay kutuları gibi uygulama öğelerini algılayabilir ve bunlarla etkileşime girebilir. Key Mapper\'ın ne yapmak istediğinizi bilmesi için, uygulama öğesiyle etkileşiminizi kaydetmeniz gerekmektedir. + Kaydetmeye başla + Kaydetmeyi durdur (%s dakika kaldı) + + %d öğe seçildi + %d öğe seçildi + + Etkileşim kuracağınız uygulamayı seçin + Tekrar kaydet + Uygulama öğesini seçin + Tuş haritanızın etkileşime geçmesini istediğiniz öğeyi seçin. + Aradığınızı bulamıyor musunuz? + Tüm uygulamalar uyumlu değildir. Uyumlu olmayan uygulamalar için bunun yerine Ekrana Dokun eylemini deneyebilirsiniz. + Olası etkileşimler + UI öğesiyle nasıl etkileşim kurmak istediğinizi seçin. + Etkileşim türünü filtrele + Ek öğeleri göster + Herhangi biri + Dokun + Dokun ve basılı tut + Odakla + İleri kaydır + Geriye kaydır + Genişlet + Daralt + Bilinmeyen: %d + Etkileşim detayları + Açıklama + Uygulama + Metin/içerik açıklaması + Sınıf adı + Araç ipucu/öneri + Kaynak kimliğini görüntüle + Özgün kimlik + Etkileşim türü + Navigasyon + Ses + Medya + Klavye + Uygulamalar + Giriş + Bağlantı + İçerik + Arayüz + Telefon + Ekran + Bildirimler + Boolean + Boolean dizisi + Integer + Tamsayı dizisi + Dize + Dize dizisi + Uzun + Uzun dizi + Bayt + Bayt dizisi + Çift + Çift dizi + Karakter + Karakter dizisi + Kayan Nokta + Kayan Nokta dizisi + Kısa + Kısa dizi + Yalnızca \"true\" veya \"false\" olabilir + \"true\" ve \"false\" içeren virgülle ayrılmış bir liste. Örn. true,false,true + Java programlama dilinde geçerli bir Tamsayı. + Java programlama dilinde geçerli Tamsayıların virgülle ayrılmış bir listesi. Örn. 100,399 + Virgülle ayrılmış bir liste. Örn. kategori1,kategori2 + Herhangi bir metin. + Dizelerin virgülle ayrılmış bir listesi. Örn. dize1,dize2 + Java programlama dilinde geçerli bir Uzun. + Java programlama dilinde geçerli Uzunların virgülle ayrılmış bir listesi. Örn. 102302234234234,399083423234429 + Java programlama dilinde geçerli bir Bayt. + Java programlama dilinde geçerli Baytların virgülle ayrılmış bir listesi. Örn. 123,3 + Java programlama dilinde geçerli bir Çift. + Java programlama dilinde geçerli Çiftlerin virgülle ayrılmış bir listesi. Örn. 1.0,3.234 + Java programlama dilinde geçerli bir Karakter. Örn. \'a\' veya \'b\' + Java programlama dilinde geçerli Karakterlerin virgülle ayrılmış bir listesi. Örn. a,b,c + Java programlama dilinde geçerli bir Kayan Nokta. Örn. 3.145 + Java programlama dilinde geçerli Kayan Noktaların virgülle ayrılmış bir listesi. Örn. 1241.123 + Java programlama dilinde geçerli bir Kısa. Örn. 2342 + Java programlama dilinde geçerli Kısa sayıların virgülle ayrılmış bir listesi. Örn. 3242,12354 + Intent bayrakları bit bayrakları olarak saklanır. Bu bayraklar Intent\'in nasıl işleneceğini değiştirir. Bir Etkinlik Intent\'i için bu alan boş bırakılırsa, Key Mapper varsayılan olarak FLAG_ACTIVITY_NEW_TASK kullanır. Daha fazla bilgi için Android geliştirici belgelerini görmek üzere \'dokümanlar\'a dokunun. + Bir eylem seçin + GitHub + Web Sitesi + Çeviriler + Sürüm %s + Oyla + Değişiklik Günlüğü + Discord + Sıkıcı şeyler + Lisans + Bu uygulamanın açık kaynak lisansı. + Gizlilik Politikası + Kişisel bilgi toplamıyoruz ama işte bunu belirten bir gizlilik politikası. + Ekibimiz + Geliştirici + Kullanıcı Deneyimi Tasarımcısı + Çevirmen (Lehçe) + Çevirmen (Çekçe) + Çevirmen (İspanyolca) + Key Mapper: Yan Tuş + Herhangi bir asistan + Yan tuş/güç düğmesi + Sesli asistan + Gelişmiş tetikleyiciler + Geliştirici, reklamların sürdürülebilir veya kullanıcı dostu bir gelir modeli olduğuna inanmıyor, bu nedenle bu ücretli tetikleyiciler geliştirmeyi desteklemeye yardımcı oluyor ❤️. Ayrıca öncelikli destek de alacaksınız. + Yan tuş ve Asistan tetikleyicisi + Yan tuşunuzu, güç düğmenizi veya cihaz asistanınızı yeniden eşleyebileceğinizi biliyor muydunuz? Asistanı veya güç menüsünü başlatmak yerine, cihazınız seçtiğiniz bir eylemi gerçekleştirebilir. Ekran kapalıyken bile çalışır! + Yan tuş tetikleyici özelliğini satın almanız gerekiyor. + Daha fazla bilgi + Kayan düğmeleri satın almanız gerekiyor. + Düğme silindi. + Kayan düğme + Yan tuş tetikleyicisini ayarla + Dikkat! + Bu tetikleyiciyi nasıl ayarlayacağınızı açıklayan web sitemizdeki talimatları okumanız gerekiyor. Key Mapper size rehberlik etmeyecek. + Talimatları oku + Tetikleyici türünü seç + Satın alma doğrulanamıyor. İnternet bağlantınız var mı? + Kilidi aç (%s) + Kullan + Yükleniyor… + Satın alındı! + Fiyatı tekrar almayı dene + Satın alma iptal edildi. + Bu, yalnızca Google Play\'den Key Mapper indirilerek satın alınabilen ücretli bir özellik gerektiriyor. + Ağ hatası oluştu. İnternet bağlantınız var mı? + Bu ürün bulunamadı. + Google Play bir hata ile karşılaştı. + Google Play\'den satın alımın başarılı olduğuna dair onay bekliyoruz. Kartınız başarıyla ücretlendirildiğinde Key Mapper uygulamasını yeniden açın. + Geçersiz satın alma. + Ödeme bekleniyor + Bir şeyler ters gitti 😕 + Tekrar dene + Geliştiriciyle iletişime geç + Yan tuş tetikleyici özelliğini satın almanız gerekiyor! Tuş eşlemesine dokunun ve ardından \'Gelişmiş tetikleyiciler\'e tıklayarak satın alın. + Kayan düğmeler özelliğini satın almanız gerekiyor! Tuş eşlemesine dokunun ve ardından \'Gelişmiş tetikleyiciler\'e tıklayarak satın alın. + Uygulamayı desteklediğiniz için teşekkürler ❤️! + Satın alma işleminiz başarılı oldu. Key Mapper\'ın ücretli bir kullanıcısı olarak uygulamayı kullanmanıza yardımcı olmak için öncelikli destek alacaksınız. Artık bu sayfada geliştiriciyle iletişime geçmek için bir düğme var. + Gelişmiş tetikleyiciler ücretli bir özelliktir ancak siz FOSS yapısını indirdiniz ve bu yapı Google Play faturalandırmasını içermiyor. Bu özelliğe erişmek için lütfen Key Mapper\'ı Google Play\'den indirin. + Play sürümünü indir + DPAD düğmelerini yeniden eşlemek ister misiniz? + Aşağıdaki adımları izleyerek Key Mapper GUI Klavyesini ayarlamanız gerekiyor. + 1. Klavye uygulamasını yükle + Yükle + Yüklendi + 2. Klavyeyi etkinleştir + Etkinleştir + Etkinleştirildi + 3. Klavyeyi kullan + Klavyeyi değiştir + Klavye seçildi + Kurulum tamamlandı! \'Bitti\'ye dokunun ve DPAD tetikleyiciniz çalışmalı. + Düğme algılanmadı mı? + Erişilebilirlik servisi yerine tetikleyicinizi kaydetmek için Key Mapper GUI Klavye uygulamasını deneyebilirsiniz. + Kurulum tamamlandı! \'Bitti\'ye dokunun ve tetikleyicinizi tekrar kaydetmeyi deneyin. Eğer çalışmazsa Android bunu yeniden eşlemeye izin vermiyor demektir 🫤. + Dokun + bu menüyü göster/gizle. + Düğmeleri herhangi bir uygulamada, hatta kilit ekranında bile yerleştirebilirsiniz. + Kapat + Yardım + Düzeni değiştir + Düğme ekle + Geri dön + Düğmeleri gizle + Düğmeleri göster + Sil + Yapılandır + Tetikleyici olarak kullan + Sil + Düğme metni (İpucu: emoji kullanın) + Düğme boyutu: + Kenar opaklığı: + Arka plan opaklığı: + İptal + Bitti + Düğmenin metni olmalı! + Kayan düğmeler + Kayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz. + Kayan düğme %s (%s) + Silinen kayan düğme + Kısıtlamalar + Bu düğmenin yalnızca bazı uygulamalarda ekranda olmasını ister misiniz? Bu tuş eşlemesi için “Kısıtlamalar” sekmesinde bir “Ön plandaki uygulama” kısıtlaması ekleyin. + Bir düzen seçin + Geri + Yardım + Yardım mı lazım? + Düğme oluştur + Düğme seç + Çık + Tetikleyici olarak kullanmadan önce bir kayan düğme oluşturmanız gerekiyor. + Tetikleyici olarak kullanmak için bir kayan düğme seçmeniz gerekiyor. + Düğmeyi yapılandır + Düzeni düzenle + Android 11 veya daha yeni bir sürüm gerektiriyor. + Yeterince düğmeniz yok mu? Artık kendi düğmelerinizi yapabilirsiniz! + Kayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz. + Tetikleyici + Eylemler + Kısıtlamalar + Seçenekler + Tuş eşlemeleri + Kayan düğmeler + Yeni tuş eşlemesi + Yeni düzen + Bir tuş eşlemesini yapılandırmak için dokunun.\nDaha fazla seçenek için uzun basın. + Bir tuş eşlemesi oluşturun! + Yeterince düğmeniz yok mu? + Kendi düğmelerinizi yapın! + Kayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz. + Bu sayfayı gizle + Key Mapper\'ı desteklediğiniz için teşekkürler ❤️! + İlk düzeninizi oluşturun! + Düzenler, kayan düğmeleri gruplar halinde organize etmenizi sağlar. Hangi düğmelerin gösterilebileceğini etkilemez. Herhangi bir düzendeki herhangi bir düğme aynı anda gösterilebilir. + %s düzeninin adını değiştir + %s düzenini sil + Kayan düğmeler + Düğme yok + Düğmeleri düzenle + Düğme ekle + Düzen adını değiştir + Kaydet + Ad boş olamaz! + Ad benzersiz olmalı! + %s sil + Bu kayan düğme düzenini silmek istediğinizden emin misiniz? + Evet, sil + İptal + Kayan düzenleri gizle + Kayan düğmeleri, bir tetikleyici oluştururken Gelişmiş Tetikleyiciler düğmesinde bulabilirsiniz. + Kayan düğmeler + Menü + Sırala + Daha fazla + Yardım + Tümünü seç + Tüm seçimi kaldır + Seçimi durdur + Bir grup yukarı çık + Duraklatıldı + + 1 uyarı + %d uyarı + + Çalışıyor + Ayarlar + Grubu sil + Hakkında + Tümünü dışa aktar + İçe aktar + Klavye seç + İçe aktarılıyor… + İçe aktarma başarılı! + Dışa aktarılıyor… + Başarısız: %s + Dosya yükleniyor… + İçe aktarma başarılı! + İçe aktarılıyor… + Hata + Uygulamayı aç + + 1 tuş eşlemesi içe aktarılıyor + %d tuş eşlemesi içe aktarılıyor + + Mevcut tuş eşlemelerinizin tümünü değiştirmek mi yoksa listeye eklemek mi istiyorsunuz? + İptal + Kapat + Ekle + Değiştir + Çoğalt + Sil + Dışa aktar + Etkin + Devre dışı + Karışık + Gruba taşı + + 1 tuş eşlemesini sil + %d tuş eşlemesini sil + + Bu tuş eşlemelerini silmek istediğinizden emin misiniz? + Evet, sil + İptal + Dosyalara kaydet + Yeni grup + Yeni alt grup + Tümünü görüntüle + Gizle + Grup kısıtlamaları + Yeni kısıtlama + Grup kısıtlamasını sil + Bu grup + Kaldır + Düzenle + Tetikleyici tuş seçenekleri + Cihaz + Asistan türü + Parmak izi hareket türü + Tıklama türü + Kayan düğme kullan + Parmak izi hareketi kullan + Yan tuş tetikleyicisi kullan + Kayan düğme + Yan tuş tetikleyicisi + Parmak izi hareketi + Kayan düğme: %s + Parmak izi okuyucuda yukarı kaydır + Parmak izi okuyucuda aşağı kaydır + Parmak izi okuyucuda sola kaydır + Parmak izi okuyucuda sağa kaydır + Gelişmiş tetikleyiciler + Kaldır + Düzenle + Test et + Tetiklendiğinde tuş eşlemesinin ne yapacağını ayarlamak için eylemler ekleyin. + Son kullanılan eylemler + Eylem seçenekleri + Sıfırla + Varsayılan: %s + Bir eylem seçin + Ara… + Burada hiçbir şey yok! + Eylemi sil + Bu eylemi silmek istediğinizden emin misiniz? + Evet, sil + İptal + Tuş eşlemesi tetiklendiğinde bu eylemleri çalıştır: + Taraf seç + Parlaklık + Minimum + Yarı + Maksimum + Test et + Android 13 veya daha yeni bir sürüm gerektiriyor. + Bu cihaz parlaklık değişikliğine izin vermiyor. + Parlaklık değişikliği + Desteklenmiyor + Tuş eşlemelerinin yalnızca belirli durumlarda çalışmasını istiyorsanız kısıtlamalar ekleyin. + Son kullanılan kısıtlamalar + Kaldır + Kısıtlamayı sil + Bu kısıtlamayı silmek istediğinizden emin misiniz? + Evet, sil + İptal + Mantık modu + Bir kısıtlama seçin + Bu tuş eşlemesi yalnızca şu durumlarda çalışır: + Adsız grup + Grup adını düzenle + Grup adını kaydet + Ad benzersiz olmalı! + Ana Sayfa + Grubu sil + %s grubunu sil + Bu grubu silmek istediğinizden emin misiniz? Bu gruptaki ve alt gruplarındaki tüm tuş eşlemeleri de silinecek! + Evet, sil + İptal + + +%d devralınan kısıtlamalar + +%d devralınan kısıtlamalar + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 933bc823e1..7b13aa6253 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,7 +25,7 @@ Restart accessibility service Share Nothing here! - Nothing here! + Key Mapper did not detect any interactions. Try showing additional elements. Stop repeating when… Trigger is released Trigger is pressed again @@ -52,7 +52,7 @@ Key Mapper log What\'s New - The sound file will be copied to Key Mapper\'s private data folder, which means your actions will still work even if the file is moved or deleted. It will also be backed up with your key maps in the zip folder. + You can either use a system ringtone or select a custom sound file.\n\nThe custom sound file will be copied to Key Mapper\'s private data folder, which means your actions will still work even if the file is moved or deleted. It will also be backed up with your key maps in the zip folder. You can delete saved sound files in the settings. Can\'t find any paired devices. Is Bluetooth turned on? @@ -90,6 +90,7 @@ The accessibility service is enabled! Your key maps should work. Extra logging is turned on! Turn this off if you aren\'t trying to fix an issue. Turn off + Turn on notifications for better on-screen messages, more actions and service updates. About @@ -107,6 +108,7 @@ %s with %d finger(s) on coordinates %d/%d to with a pinch distance of %dpx %dms (%s) Call %s Play sound: %s + Play unknown sound @@ -409,6 +411,7 @@ Set flags No limit Choose sound file + Choose system ringtone Edit action Replace action @@ -507,6 +510,9 @@ Unrecognized key code The pressed button was not recognized by the input system. In the past Key Mapper detected such buttons as one and the same. Currently the app tries to distinguish the button based on the scan code, which should be more unique. However, this is a makeshift incomplete solution, which doesn\'t guarantee uniqueness. + Turn on notifications + Some actions and options need this permission to work. You can also get notified when there is important news about the app. + Done Guide Guide @@ -519,11 +525,13 @@ Discard changes Save Understood + Turn on Turn off Cancel Don\'t show again Keep editing + No thanks Hide Online guide @@ -626,9 +634,8 @@ Show an on-screen message when automatically changing the keyboard - Key Mapper has root permission - Enable this if you want to use features/actions which only work on rooted devices. Key Mapper must have root permission from your root-access-management app (e.g Magisk, SuperSU) for these features to work. - Only turn this on if you know your device is rooted and you have given Key Mapper root permission. + Request root permission + If your device is rooted then this will show the root permission pop up from Magisk or your root app. Choose theme Light and dark themes available @@ -690,6 +697,9 @@ Are you sure you want to reset all settings in the app to the default? Your key maps will NOT be reset. The introduction screen and all warning pop ups will show again. Yes, reset + PRO mode + Advanced detection of key events.\ + @@ -981,11 +991,11 @@ Next track Next track for an app - Next track for %s> + Next track for %s Previous track Previous track for an app - Previous track for %s> + Previous track for %s Fast forward Fast forward for an app @@ -994,9 +1004,21 @@ Rewind Rewind for an app - Rewind for %s> + Rewind for %s Not all media apps support rewinding. E.g Google Play Music. + Stop media + Stop media for an app + Stop media for %s + + Step media forward + Step media forward for an app + Step media forward for %s + + Step media backward + Step media backward for an app + Step media backward for %s + Go back Go home Open recents @@ -1104,29 +1126,28 @@ You must prepend \'Bearer\' if necessary Interact with app element - Key Mapper can detect and interact with app elements like menus, tabs, buttons and checkboxes. You need to record yourself interacting with the app element so that Key Mapper knows what you want to do. + Key Mapper can detect and interact with app elements like menus, tabs, buttons and checkboxes. You need to record yourself interacting with the app so that Key Mapper knows what you want to do. Start recording Stop recording (%s min left) - Go to another app and interact with it. Key Mapper will record what you do and you can choose which interactions you want to use in your key map. Open Key Mapper again when you’re done. - - %d interaction detected - %d interactions detected + + %d element detected + %d elements detected - Choose the app to interact with + Choose the app to interact with Record again Choose app element Choose the element you want your key map to interact with. Can\'t find what you’re looking for? Not all apps are compatible. For incompatible apps you can try the Tap Screen action instead. - Interaction type + Possible interactions Select how you want to interact with the UI element. Filter interaction type + Show additional elements Any Tap Tap and hold Focus - Select Scroll forward Scroll backward Expand @@ -1136,8 +1157,9 @@ Interaction details Description App - Text / content description + Text/content description Class name + Tooltip/hint View resource ID Unique ID Interaction types @@ -1150,13 +1172,14 @@ Keyboard Apps Input - Camera & Sound + Flashlight Connectivity Content Interface Phone Display Notifications + Special @@ -1203,8 +1226,18 @@ - Quick Start Guide - Check out the Quick Start Guide if you are stuck. + Skip tutorial + Create your first key map! + A key map is a rule to tell your device what to do when a button is pressed. + Record a trigger + Tap record, and then press the physical buttons you want to change. + Love Key Mapper? ❤️ + For an affordable price, make powerful key maps with on-screen floating buttons and support the development of more great features 👨‍💻. + + Choose an action + An action is what should happen when you press the trigger. + Choose a constraint (optional) + If you want the key map to only work in certain situations, for example, when an app is open. @@ -1271,6 +1304,7 @@ Unlock (%s) Use Loading… + Purchased! Retry fetching price Purchase cancelled. This requires a paid feature that can only be bought by downloading Key Mapper from Google Play. @@ -1532,4 +1566,28 @@ +%d inherited constraint +%d inherited constraints + + + + PRO mode + Important! + These settings are dangerous and can cause your buttons to stop working if you set them incorrectly.\n\nIf you make a mistake, you may need to force restart your device by holding down the power button for a long time. + %d… + I understand + Understood + Set up + Root detected + You can skip the set up process by giving Key Mapper root permission. This will let Key Mapper auto start PRO mode on boot as well. + Use root + Shizuku detected + You can skip the set up process by giving Key Mapper Shizuku permission. + Use Shizuku + Set up with Key Mapper + Continue + Options + Enable PRO mode for all key maps + Key Mapper will use the ADB Shell for remapping + These settings are unavailable until you acknowledge the warning. + + 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 a76df90b1a..07815d2b57 100644 --- a/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt @@ -20,7 +20,7 @@ import io.github.sds100.keymapper.data.repositories.FakePreferenceRepository import io.github.sds100.keymapper.data.repositories.FloatingLayoutRepository import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.mappings.keymaps.KeyMapRepository +import io.github.sds100.keymapper.keymaps.KeyMapRepository import io.github.sds100.keymapper.system.files.FakeFileAdapter import io.github.sds100.keymapper.system.files.IFile import io.github.sds100.keymapper.system.files.JavaFile @@ -136,6 +136,61 @@ class BackupManagerTest { Dispatchers.resetMain() } + /** + * Issue #1655. If the list of groups in the backup has a child before the parent then the + * parent must be restored first. Otherwise the SqliteConstraintException will be thrown. + */ + @Test + fun `restore groups breadth first so parents exist before children are restored with child first in the backup`() = runTest(testDispatcher) { + val parentGroup1 = GroupEntity( + uid = "parent_group_1_uid", + name = "parent_group_1_name", + parentUid = null, + lastOpenedDate = 0L, + ) + + val parentGroup2 = GroupEntity( + uid = "parent_group_2_uid", + name = "parent_group_2_name", + parentUid = null, + lastOpenedDate = 0L, + ) + + val childGroup = GroupEntity( + uid = "child_group_uid", + name = "child_group_name", + parentUid = parentGroup1.uid, + lastOpenedDate = 0L, + ) + + val grandChildGroup = GroupEntity( + uid = "grand_child_group_uid", + name = "grand_child_group_name", + parentUid = childGroup.uid, + lastOpenedDate = 0L, + ) + + val backupContent = BackupContent( + appVersion = Constants.VERSION_CODE, + dbVersion = AppDatabase.DATABASE_VERSION, + groups = listOf(childGroup, grandChildGroup, parentGroup1), + ) + + inOrder(mockGroupRepository) { + backupManager.restore( + RestoreType.REPLACE, + backupContent, + emptyList(), + currentTime = 0L, + ) + + verify(mockGroupRepository).insert(parentGroup1) + verify(mockGroupRepository).insert(childGroup) + verify(mockGroupRepository).insert(grandChildGroup) + verify(mockGroupRepository, never()).update(any()) + } + } + /** * Issue #1655. If the list of groups in the backup has a child before the parent then the * parent must be restored first. Otherwise the SqliteConstraintException will be thrown. @@ -184,10 +239,10 @@ class BackupManagerTest { currentTime = 0L, ) + verify(mockGroupRepository).insert(parentGroup2) verify(mockGroupRepository).insert(parentGroup1) verify(mockGroupRepository).insert(childGroup) verify(mockGroupRepository).insert(grandChildGroup) - verify(mockGroupRepository).insert(parentGroup2) verify(mockGroupRepository, never()).update(any()) } } 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 fa87b9068a..d80b0cbe1e 100644 --- a/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt @@ -4,18 +4,17 @@ import android.view.KeyEvent import io.github.sds100.keymapper.actions.Action 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.FingerprintGestureType -import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCaseController -import io.github.sds100.keymapper.mappings.keymaps.KeyMap -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.KeyEventDetectionSource -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.keymaps.ClickType +import io.github.sds100.keymapper.keymaps.ConfigKeyMapUseCaseController +import io.github.sds100.keymapper.keymaps.FingerprintGestureType +import io.github.sds100.keymapper.keymaps.KeyMap import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.trigger.AssistantTriggerKey +import io.github.sds100.keymapper.trigger.AssistantTriggerType +import io.github.sds100.keymapper.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.trigger.Trigger +import io.github.sds100.keymapper.trigger.TriggerKeyDevice +import io.github.sds100.keymapper.trigger.TriggerMode import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.dataOrNull import io.github.sds100.keymapper.util.singleKeyTrigger @@ -236,7 +235,10 @@ class ConfigKeyMapUseCaseTest { val trigger = useCase.keyMap.value.dataOrNull()!!.trigger assertThat(trigger.keys, hasSize(2)) - assertThat(trigger.keys[0], instanceOf(KeyCodeTriggerKey::class.java)) + assertThat( + trigger.keys[0], + instanceOf(_root_ide_package_.io.github.sds100.keymapper.trigger.KeyCodeTriggerKey::class.java), + ) assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) } @@ -255,7 +257,10 @@ class ConfigKeyMapUseCaseTest { val trigger = useCase.keyMap.value.dataOrNull()!!.trigger assertThat(trigger.keys, hasSize(2)) - assertThat(trigger.keys[0], instanceOf(KeyCodeTriggerKey::class.java)) + assertThat( + trigger.keys[0], + instanceOf(_root_ide_package_.io.github.sds100.keymapper.trigger.KeyCodeTriggerKey::class.java), + ) assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) } 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 index 91aad88f8d..608de9dcfa 100644 --- a/app/src/test/java/io/github/sds100/keymapper/actions/GetActionFailedUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/actions/GetActionFailedUseCaseTest.kt @@ -53,6 +53,7 @@ class GetActionFailedUseCaseTest { cameraAdapter = mock(), soundsManager = mock(), shizukuAdapter = mockShizukuAdapter, + ringtoneAdapter = mock(), ) } @@ -60,58 +61,56 @@ class GetActionFailedUseCaseTest { * #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.actionErrorSnapshot.first().getError(action) - - // THEN - assertThat(error, nullValue()) + 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.actionErrorSnapshot.first().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.actionErrorSnapshot.first().getError(action) - - // THEN - assertThat(error, `is`(Error.ShizukuNotStarted)) + 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.actionErrorSnapshot.first().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 b338b65d2c..23ebbb1ca7 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 @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.actions +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.system.devices.FakeDevicesAdapter @@ -58,7 +59,7 @@ class PerformActionsUseCaseTest { inputMethodAdapter = mock(), fileAdapter = mock(), suAdapter = mock { - on { isGranted }.then { MutableStateFlow(false) } + on { isRooted }.then { MutableStateFlow(false) } }, shellAdapter = mock(), intentAdapter = mock(), @@ -85,6 +86,7 @@ class PerformActionsUseCaseTest { shizukuInputEventInjector = mock(), permissionAdapter = mock(), notificationReceiverAdapter = mock(), + ringtoneAdapter = mock(), ) } @@ -142,6 +144,7 @@ class PerformActionsUseCaseTest { deviceId = fakeGamePad.id, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_GAMEPAD, ) verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) @@ -171,6 +174,7 @@ class PerformActionsUseCaseTest { deviceId = 0, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_GAMEPAD, ) verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) @@ -220,6 +224,7 @@ class PerformActionsUseCaseTest { deviceId = fakeKeyboard.id, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_GAMEPAD, ) verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) @@ -278,6 +283,7 @@ class PerformActionsUseCaseTest { deviceId = 11, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_KEYBOARD, ), ) } @@ -318,6 +324,7 @@ class PerformActionsUseCaseTest { deviceId = 10, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_KEYBOARD, ), ) } 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 e28c7f0db1..298c1dc130 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 @@ -46,7 +46,7 @@ class ConfigKeyServiceEventActionViewModelTest { @Before fun init() { Dispatchers.setMain(testDispatcher) - inputDevices = MutableStateFlow(emptyList()) + inputDevices = MutableStateFlow(emptyList()) mockUseCase = mock { on { showDeviceDescriptors }.then { MutableStateFlow(false) } diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt b/app/src/test/java/io/github/sds100/keymapper/keymaps/DpadMotionEventTrackerTest.kt similarity index 97% rename from app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt rename to app/src/test/java/io/github/sds100/keymapper/keymaps/DpadMotionEventTrackerTest.kt index b323630108..732c7d92b0 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/keymaps/DpadMotionEventTrackerTest.kt @@ -1,7 +1,8 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps +import android.view.InputDevice import android.view.KeyEvent -import io.github.sds100.keymapper.mappings.keymaps.detection.DpadMotionEventTracker +import io.github.sds100.keymapper.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.system.inputevents.MyKeyEvent import io.github.sds100.keymapper.system.inputevents.MyMotionEvent @@ -70,6 +71,7 @@ class DpadMotionEventTrackerTest { scanCode = 0, device = CONTROLLER_1_DEVICE, repeatCount = 0, + source = InputDevice.SOURCE_DPAD, ), ), ) @@ -83,6 +85,7 @@ class DpadMotionEventTrackerTest { scanCode = 0, device = CONTROLLER_1_DEVICE, repeatCount = 0, + source = InputDevice.SOURCE_DPAD, ), ), ) @@ -277,6 +280,7 @@ class DpadMotionEventTrackerTest { scanCode = 0, device = device, repeatCount = 0, + source = 0, ) } @@ -288,6 +292,7 @@ class DpadMotionEventTrackerTest { scanCode = 0, device = device, repeatCount = 0, + source = 0, ) } } diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/keymaps/KeyMapControllerTest.kt similarity index 99% rename from app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt rename to app/src/test/java/io/github/sds100/keymapper/keymaps/KeyMapControllerTest.kt index 149d0d7522..c068b9cb03 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/keymaps/KeyMapControllerTest.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import android.view.KeyEvent import androidx.arch.core.executor.testing.InstantTaskExecutorRule @@ -12,22 +12,20 @@ import io.github.sds100.keymapper.constraints.ConstraintMode 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.mappings.ClickType -import io.github.sds100.keymapper.mappings.FingerprintGestureType -import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapModel -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.FingerprintTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource -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 +import io.github.sds100.keymapper.keymaps.detection.DetectKeyMapModel +import io.github.sds100.keymapper.keymaps.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.detection.KeyMapController import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.system.inputevents.MyKeyEvent import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.trigger.FingerprintTriggerKey +import io.github.sds100.keymapper.trigger.KeyCodeTriggerKey +import io.github.sds100.keymapper.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.trigger.Trigger +import io.github.sds100.keymapper.trigger.TriggerKey +import io.github.sds100.keymapper.trigger.TriggerKeyDevice +import io.github.sds100.keymapper.trigger.TriggerMode import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.TestConstraintSnapshot @@ -1166,6 +1164,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) // If both triggers are detected @@ -1180,6 +1179,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) // If no triggers are detected @@ -1194,6 +1194,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) } } @@ -2665,6 +2666,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( @@ -2673,6 +2675,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) } } @@ -3068,6 +3071,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) } @@ -3099,6 +3103,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) } @@ -3126,6 +3131,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } @@ -4137,6 +4143,7 @@ class KeyMapControllerTest { scanCode = scanCode, device = device, repeatCount = repeatCount, + source = 0, ), ) diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ProcessKeyMapGroupsForDetectionTest.kt b/app/src/test/java/io/github/sds100/keymapper/keymaps/ProcessKeyMapGroupsForDetectionTest.kt similarity index 97% rename from app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ProcessKeyMapGroupsForDetectionTest.kt rename to app/src/test/java/io/github/sds100/keymapper/keymaps/ProcessKeyMapGroupsForDetectionTest.kt index 2f093a3899..ac2ab51cf3 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ProcessKeyMapGroupsForDetectionTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/keymaps/ProcessKeyMapGroupsForDetectionTest.kt @@ -1,11 +1,11 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.constraints.Constraint import io.github.sds100.keymapper.constraints.ConstraintMode import io.github.sds100.keymapper.constraints.ConstraintState import io.github.sds100.keymapper.groups.Group -import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapModel -import io.github.sds100.keymapper.mappings.keymaps.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.keymaps.detection.DetectKeyMapModel +import io.github.sds100.keymapper.keymaps.detection.DetectKeyMapsUseCaseImpl import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.junit.Test diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt similarity index 93% rename from app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt rename to app/src/test/java/io/github/sds100/keymapper/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt index 614d9d3188..021d19bc77 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.mappings.keymaps +package io.github.sds100.keymapper.keymaps import io.github.sds100.keymapper.actions.Action import io.github.sds100.keymapper.actions.ActionData @@ -6,9 +6,9 @@ import io.github.sds100.keymapper.actions.ActionErrorSnapshot 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.mappings.keymaps.detection.DetectKeyMapsUseCase -import io.github.sds100.keymapper.mappings.keymaps.detection.TriggerKeyMapFromOtherAppsController -import io.github.sds100.keymapper.mappings.keymaps.trigger.Trigger +import io.github.sds100.keymapper.keymaps.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.keymaps.detection.TriggerKeyMapFromOtherAppsController +import io.github.sds100.keymapper.trigger.Trigger import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.TestConstraintSnapshot import junitparams.JUnitParamsRunner diff --git a/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt b/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt index d212af3db8..eb4a9c26c3 100644 --- a/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt +++ b/app/src/test/java/io/github/sds100/keymapper/util/KeyMapUtils.kt @@ -1,12 +1,11 @@ package io.github.sds100.keymapper.util -import io.github.sds100.keymapper.mappings.ClickType -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource -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 +import io.github.sds100.keymapper.keymaps.ClickType +import io.github.sds100.keymapper.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.trigger.Trigger +import io.github.sds100.keymapper.trigger.TriggerKey +import io.github.sds100.keymapper.trigger.TriggerKeyDevice +import io.github.sds100.keymapper.trigger.TriggerMode /** * Created by sds100 on 19/04/2021. @@ -33,7 +32,7 @@ fun triggerKey( clickType: ClickType = ClickType.SHORT_PRESS, consume: Boolean = true, detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, -): KeyCodeTriggerKey = KeyCodeTriggerKey( +): _root_ide_package_.io.github.sds100.keymapper.trigger.KeyCodeTriggerKey = _root_ide_package_.io.github.sds100.keymapper.trigger.KeyCodeTriggerKey( keyCode = keyCode, device = device, clickType = clickType, diff --git a/app/version.properties b/app/version.properties index c6beedf368..9702277f1d 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=3.1.0 -VERSION_CODE=104 +VERSION_NAME=3.1.2 +VERSION_CODE=122 VERSION_NUM=0 \ No newline at end of file diff --git a/build.gradle b/build.gradle index e17e83582b..74e6f8a1da 100644 --- a/build.gradle +++ b/build.gradle @@ -13,15 +13,14 @@ buildscript { def nav_version = '2.6.0' classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" - classpath 'com.android.tools.build:gradle:8.9.1' + classpath 'com.android.tools.build:gradle:8.10.0' 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" classpath "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:2.1.0" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.1.0-1.0.28" classpath "androidx.room:androidx.room.gradle.plugin:2.6.1" - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files + classpath "dev.rikka.tools.refine:gradle-plugin:4.4.0" } } @@ -38,4 +37,3 @@ allprojects { task clean(type: Delete) { delete rootProject.buildDir } - diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt index 4efb85f341..9810cafe1e 100644 --- a/fastlane/metadata/android/en-US/title.txt +++ b/fastlane/metadata/android/en-US/title.txt @@ -1 +1 @@ -Key Mapper & Floating buttons \ No newline at end of file +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/full_description.txt b/fastlane/metadata/android/tr_TR/full_description.txt new file mode 100644 index 0000000000..b5be52ef5e --- /dev/null +++ b/fastlane/metadata/android/tr_TR/full_description.txt @@ -0,0 +1,55 @@ +# Klavyenizde veya oyun kumandanızda özel makrolar oluşturun, herhangi bir uygulamada ekran üstü düğmeler yapın ve ses düğmelerinizden yeni işlevler açın! + +Key Mapper, çok çeşitli düğme ve tuşları destekler*: + +- TÜM telefon düğmeleriniz (ses VE yan tuş) +- Oyun kumandaları (D-pad, ABXY ve çoğu diğer tuşlar) +- Klavyeler +- Kulaklık setleri ve kulaklıklar +- Parmak izi sensörü + +Yeterli tuş yok mu? Kendi ekran üstü düğme düzenlerinizi tasarlayın ve bunları gerçek tuşlar gibi yeniden atayın! + + +## Ne tür kısayollar oluşturabilirim? +-------------------------- + +100'den fazla bireysel eylemle, sınır gökyüzüdür. +Ekran dokunuşları ve hareketleri, klavye girişleri, uygulama açma, medya kontrolü ve hatta diğer uygulamalara doğrudan intent gönderme ile karmaşık makrolar oluşturun. + + +## Ne kadar kontrole sahibim? +--------------------------- + +TETİKLEYİCİLER: Bir tuş haritasını nasıl tetikleyeceğinize siz karar verirsiniz. Uzun basma, çift basma, istediğiniz kadar basma! Farklı cihazlardaki tuşları birleştirin ve hatta ekran üstü düğmelerinizi de dahil edin. + +EYLEMLER: Yapmak istediğiniz şey için özel makrolar tasarlayın. 100'den fazla eylemi birleştirin ve her biri arasındaki gecikmeyi seçin. Yavaş görevleri otomatikleştirmek ve hızlandırmak için tekrarlayan eylemler ayarlayın. + +KISITLAMALAR: Tuş haritalarının ne zaman çalışacağını ve ne zaman çalışmayacağını siz seçersiniz. Sadece belirli bir uygulamada mı gerekli? Ya da medya oynatılırken mi? Kilit ekranınızda mı? Maksimum kontrol için tuş haritalarınızı kısıtlayın. + +* Çoğu cihaz zaten desteklenmektedir ve zamanla yeni cihazlar eklenmektedir. Sizin için çalışmıyorsa bize bildirin, cihazınıza öncelik verebiliriz. + +Şu anda desteklenmeyen: + - Fare düğmeleri + - Oyun kumandalarındaki joystick ve tetikler (LT, RT) + + +Güvenlik ve erişilebilirlik hizmetleri +--------------------------- + +Bu uygulama, odaktaki uygulamayı algılamak ve tuş basımlarını kullanıcı tarafından tanımlanan tuş haritalarına uyarlamak için Android Erişilebilirlik API’sini kullanan Key Mapper Erişilebilirlik hizmetimizi içermektedir. Ayrıca, diğer uygulamaların üzerinde yardımcı Floating Button (Yüzen Düğme) katmanları çizmek için de kullanılmaktadır. + +Erişilebilirlik hizmetini çalıştırmayı kabul ettiğinizde, uygulama cihazınızı kullanırken tuş vuruşlarını izleyebilecektir. Ayrıca, uygulamada bu hareketleri kullanıyorsanız, kaydırma ve yakınlaştırma/daraltma hareketlerini de taklit edecektir. + +Herhangi bir kullanıcı verisi toplamayacak veya herhangi bir veriyi göndermek üzere internete bağlanmayacaktır. + +Erişilebilirlik hizmetimiz, yalnızca kullanıcı cihazındaki fiziksel bir tuşa bastığında tetiklenir. Kullanıcı, sistem erişilebilirlik ayarlarından bu hizmeti istediği zaman kapatabilir. + +Discord topluluğumuza gelip merhaba deyin! +www.keymapper.club + +Kodu kendiniz görün! (Açık kaynak) +code.keymapper.club + +Belgeleri okuyun: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/short_description.txt b/fastlane/metadata/android/tr_TR/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/tr_TR/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/title.txt b/fastlane/metadata/android/tr_TR/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/tr_TR/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/nativelib/.gitignore b/nativelib/.gitignore new file mode 100644 index 0000000000..c591fdeb45 --- /dev/null +++ b/nativelib/.gitignore @@ -0,0 +1,2 @@ +/build +.cxx \ No newline at end of file diff --git a/nativelib/build.gradle.kts b/nativelib/build.gradle.kts new file mode 100644 index 0000000000..78a158aa84 --- /dev/null +++ b/nativelib/build.gradle.kts @@ -0,0 +1,83 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +// id("dev.rikka.tools.refine") +} + +android { + namespace = "io.github.sds100.keymapper.nativelib" + compileSdk = 35 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + + externalNativeBuild { + cmake { + // -DANDROID_STL=none is required by Rikka's library: https://github.com/RikkaW/libcxx-prefab + // -DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON is required to get the app running on the Android 15. This is related to the new 16kB page size support. + arguments("-DANDROID_STL=none", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON") + } + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + buildFeatures { + aidl = true + } + + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + + packaging { + jniLibs { + useLegacyPackaging = false + + // This is required on Android 15. Otherwise a java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH error is thrown. + keepDebugSymbols.add("**/*.so") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + compileOnly(project(":systemstubs")) + implementation("org.conscrypt:conscrypt-android:2.5.3") + implementation("androidx.core:core-ktx:1.16.0") + + // From Shizuku :manager module build.gradle file. + implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") + implementation("org.bouncycastle:bcpkix-jdk15on:1.70") + implementation("me.zhanghai.android.appiconloader:appiconloader:1.5.0") + implementation("dev.rikka.rikkax.core:core-ktx:1.4.1") +// implementation("dev.rikka.hidden:compat:4.3.3") +// compileOnly("dev.rikka.hidden:stub:4.3.3") +// +// implementation("dev.rikka.tools.refine:runtime:4.3.0") +// annotationProcessor("dev.rikka.tools.refine:annotation-processor:4.3.0") +// compileOnly("dev.rikka.tools.refine:annotation:4.3.0") + +} diff --git a/nativelib/consumer-rules.pro b/nativelib/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nativelib/proguard-rules.pro b/nativelib/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/nativelib/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/nativelib/src/main/AndroidManifest.xml b/nativelib/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a5918e68ab --- /dev/null +++ b/nativelib/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/nativelib/src/main/aidl/io/github/sds100/keymapper/nativelib/IEvdevService.aidl b/nativelib/src/main/aidl/io/github/sds100/keymapper/nativelib/IEvdevService.aidl new file mode 100644 index 0000000000..678e8c1cc7 --- /dev/null +++ b/nativelib/src/main/aidl/io/github/sds100/keymapper/nativelib/IEvdevService.aidl @@ -0,0 +1,10 @@ +package io.github.sds100.keymapper.nativelib; + +interface IEvdevService { + // Destroy method defined by Shizuku server. This is required + // for Shizuku user services. + // See demo/service/UserService.java in the Shizuku-API repository. + void destroy() = 16777114; + + String sendEvent() = 1; +} \ No newline at end of file diff --git a/nativelib/src/main/cpp/CMakeLists.txt b/nativelib/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..417847fe0c --- /dev/null +++ b/nativelib/src/main/cpp/CMakeLists.txt @@ -0,0 +1,43 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html. +# For more examples on how to use CMake, see https://github.com/android/ndk-samples. + +# Sets the minimum CMake version required for this project. +cmake_minimum_required(VERSION 3.22.1) + +# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, +# Since this is the top level CMakeLists.txt, the project name is also accessible +# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level +# build script scope). +project("nativelib") + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. +# +# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define +# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} +# is preferred for the same purpose. +# +# In order to load a library into your app from Java/Kotlin, you must call +# System.loadLibrary() and pass the name of the library defined here; +# for GameActivity/NativeActivity derived applications, the same library name must be +# used in the AndroidManifest.xml file. +add_library(${CMAKE_PROJECT_NAME} SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + nativelib.cpp + libevdev/libevdev.c + libevdev/libevdev-names.c + libevdev/libevdev-uinput.c +) + +include_directories(src/main/cpp/include/) + +# Specifies libraries CMake should link to your target library. You +# can link libraries from various origins, such as libraries defined in this +# build script, prebuilt third-party libraries, or Android system libraries. +target_link_libraries(${CMAKE_PROJECT_NAME} + # List libraries link to the target library + android + log) \ No newline at end of file diff --git a/nativelib/src/main/cpp/libevdev/.gitignore b/nativelib/src/main/cpp/libevdev/.gitignore new file mode 100644 index 0000000000..339d72d3b8 --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/.gitignore @@ -0,0 +1 @@ +event-names.h diff --git a/nativelib/src/main/cpp/libevdev/Makefile.am b/nativelib/src/main/cpp/libevdev/Makefile.am new file mode 100644 index 0000000000..f577900827 --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/Makefile.am @@ -0,0 +1,42 @@ +lib_LTLIBRARIES=libevdev.la + +AM_CPPFLAGS = $(GCC_CFLAGS) $(GCOV_CFLAGS) -I$(top_srcdir)/include -I$(top_srcdir) +AM_LDFLAGS = $(GCOV_LDFLAGS) + +libevdev_la_SOURCES = \ + libevdev.h \ + libevdev-int.h \ + libevdev-util.h \ + libevdev-uinput.c \ + libevdev-uinput.h \ + libevdev-uinput-int.h \ + libevdev.c \ + libevdev-names.c \ + ../include/linux/input.h \ + ../include/linux/uinput.h \ + ../include/linux/@OS@/input-event-codes.h \ + ../include/linux/@OS@/input.h \ + ../include/linux/@OS@/uinput.h + +libevdev_la_LDFLAGS = \ + $(AM_LDFLAGS) \ + -version-info $(LIBEVDEV_LT_VERSION) \ + -Wl,--version-script="$(srcdir)/libevdev.sym" \ + $(GNU_LD_FLAGS) + +EXTRA_libevdev_la_DEPENDENCIES = $(srcdir)/libevdev.sym + +libevdevincludedir = $(includedir)/libevdev-1.0/libevdev +libevdevinclude_HEADERS = libevdev.h libevdev-uinput.h + +event-names.h: Makefile make-event-names.py + $(PYTHON) $(srcdir)/make-event-names.py $(top_srcdir)/include/linux/@OS@/input.h $(top_srcdir)/include/linux/@OS@/input-event-codes.h > $@ + + +EXTRA_DIST = make-event-names.py libevdev.sym ../include +CLEANFILES = event-names.h +BUILT_SOURCES = event-names.h + +if GCOV_ENABLED +CLEANFILES += *.gcno +endif diff --git a/nativelib/src/main/cpp/libevdev/libevdev-int.h b/nativelib/src/main/cpp/libevdev/libevdev-int.h new file mode 100644 index 0000000000..672fd2f48f --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev-int.h @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +#ifndef LIBEVDEV_INT_H +#define LIBEVDEV_INT_H + +#include +#include +#include +#include +#include "libevdev.h" +#include "libevdev-util.h" + +#define MAX_NAME 256 +#define ABS_MT_MIN ABS_MT_SLOT +#define ABS_MT_MAX ABS_MT_TOOL_Y +#define ABS_MT_CNT (ABS_MT_MAX - ABS_MT_MIN + 1) +#define LIBEVDEV_EXPORT __attribute__((visibility("default"))) +#define ALIAS(_to) __attribute__((alias(#_to))) + +/** + * Sync state machine: + * default state: SYNC_NONE + * + * SYNC_NONE → SYN_DROPPED or forced sync → SYNC_NEEDED + * SYNC_NEEDED → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC) → SYNC_IN_PROGRESS + * SYNC_NEEDED → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC_NONE) → SYNC_NONE + * SYNC_IN_PROGRESS → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC_NONE) → SYNC_NONE + * SYNC_IN_PROGRESS → no sync events left → SYNC_NONE + * + */ +enum SyncState { + SYNC_NONE, + SYNC_NEEDED, + SYNC_IN_PROGRESS, +}; + +/** + * Internal only: log data used to send messages to the respective log + * handler. We re-use the same struct for a global and inside + * struct libevdev. + * For the global, device_handler is NULL, for per-device instance + * global_handler is NULL. + */ +struct logdata { + enum libevdev_log_priority priority; /** minimum logging priority */ + libevdev_log_func_t global_handler; /** global handler function */ + libevdev_device_log_func_t device_handler; /** per-device handler function */ + void *userdata; /** user-defined data pointer */ +}; + +struct libevdev { + int fd; + bool initialized; + char *name; + char *phys; + char *uniq; + struct input_id ids; + int driver_version; + unsigned long bits[NLONGS(EV_CNT)]; + unsigned long props[NLONGS(INPUT_PROP_CNT)]; + unsigned long key_bits[NLONGS(KEY_CNT)]; + unsigned long rel_bits[NLONGS(REL_CNT)]; + unsigned long abs_bits[NLONGS(ABS_CNT)]; + unsigned long led_bits[NLONGS(LED_CNT)]; + unsigned long msc_bits[NLONGS(MSC_CNT)]; + unsigned long sw_bits[NLONGS(SW_CNT)]; + unsigned long rep_bits[NLONGS(REP_CNT)]; /* convenience, always 1 */ + unsigned long ff_bits[NLONGS(FF_CNT)]; + unsigned long snd_bits[NLONGS(SND_CNT)]; + unsigned long key_values[NLONGS(KEY_CNT)]; + unsigned long led_values[NLONGS(LED_CNT)]; + unsigned long sw_values[NLONGS(SW_CNT)]; + struct input_absinfo abs_info[ABS_CNT]; + int *mt_slot_vals; /* [num_slots * ABS_MT_CNT] */ + int num_slots; /**< valid slots in mt_slot_vals */ + int current_slot; + int rep_values[REP_CNT]; + + enum SyncState sync_state; + enum libevdev_grab_mode grabbed; + + struct input_event *queue; + size_t queue_size; /**< size of queue in elements */ + size_t queue_next; /**< next event index */ + size_t queue_nsync; /**< number of sync events */ + + struct timeval last_event_time; + + struct logdata log; +}; + +#define log_msg_cond(dev, priority, ...) \ + do { \ + if (_libevdev_log_priority(dev) >= priority) \ + _libevdev_log_msg(dev, priority, __FILE__, __LINE__, __func__, __VA_ARGS__); \ + } while(0) + +#define log_error(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_ERROR, __VA_ARGS__) +#define log_info(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_INFO, __VA_ARGS__) +#define log_dbg(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_DEBUG, __VA_ARGS__) +#define log_bug(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_ERROR, "BUG: "__VA_ARGS__) + +extern void +_libevdev_log_msg(const struct libevdev *dev, + enum libevdev_log_priority priority, + const char *file, int line, const char *func, + const char *format, ...) LIBEVDEV_ATTRIBUTE_PRINTF(6, 7); +extern enum libevdev_log_priority +_libevdev_log_priority(const struct libevdev *dev); + +static inline void +init_event(struct libevdev *dev, struct input_event *ev, int type, int code, int value) +{ + ev->input_event_sec = dev->last_event_time.tv_sec; + ev->input_event_usec = dev->last_event_time.tv_usec; + ev->type = type; + ev->code = code; + ev->value = value; +} + +/** + * @return a pointer to the next element in the queue, or NULL if the queue + * is full. + */ +static inline struct input_event* +queue_push(struct libevdev *dev) +{ + if (dev->queue_next >= dev->queue_size) + return NULL; + + return &dev->queue[dev->queue_next++]; +} + +static inline bool +queue_push_event(struct libevdev *dev, unsigned int type, + unsigned int code, int value) +{ + struct input_event *ev = queue_push(dev); + + if (ev) + init_event(dev, ev, type, code, value); + + return ev != NULL; +} + +/** + * Set ev to the last element in the queue, removing it from the queue. + * + * @return 0 on success, 1 if the queue is empty. + */ +static inline int +queue_pop(struct libevdev *dev, struct input_event *ev) +{ + if (dev->queue_next == 0) + return 1; + + *ev = dev->queue[--dev->queue_next]; + + return 0; +} + +static inline int +queue_peek(struct libevdev *dev, size_t idx, struct input_event *ev) +{ + if (dev->queue_next == 0 || idx > dev->queue_next) + return 1; + *ev = dev->queue[idx]; + return 0; +} + +/** + * Shift the first n elements into ev and return the number of elements + * shifted. + * ev must be large enough to store n elements. + * + * @param ev The buffer to copy into, or NULL + * @return The number of elements in ev. + */ +static inline int +queue_shift_multiple(struct libevdev *dev, size_t n, struct input_event *ev) +{ + size_t remaining; + + if (dev->queue_next == 0) + return 0; + + remaining = dev->queue_next; + n = min(n, remaining); + remaining -= n; + + if (ev) + memcpy(ev, dev->queue, n * sizeof(*ev)); + + memmove(dev->queue, &dev->queue[n], remaining * sizeof(*dev->queue)); + + dev->queue_next = remaining; + return n; +} + +/** + * Set ev to the first element in the queue, shifting everything else + * forward by one. + * + * @return 0 on success, 1 if the queue is empty. + */ +static inline int +queue_shift(struct libevdev *dev, struct input_event *ev) +{ + return queue_shift_multiple(dev, 1, ev) == 1 ? 0 : 1; +} + +static inline int +queue_alloc(struct libevdev *dev, size_t size) +{ + if (size == 0) + return -ENOMEM; + + dev->queue = calloc(size, sizeof(struct input_event)); + if (!dev->queue) + return -ENOMEM; + + dev->queue_size = size; + dev->queue_next = 0; + return 0; +} + +static inline void +queue_free(struct libevdev *dev) +{ + free(dev->queue); + dev->queue_size = 0; + dev->queue_next = 0; +} + +static inline size_t +queue_num_elements(struct libevdev *dev) +{ + return dev->queue_next; +} + +static inline size_t +queue_size(struct libevdev *dev) +{ + return dev->queue_size; +} + +static inline size_t +queue_num_free_elements(struct libevdev *dev) +{ + if (dev->queue_size == 0) + return 0; + + return dev->queue_size - dev->queue_next; +} + +static inline struct input_event * +queue_next_element(struct libevdev *dev) +{ + if (dev->queue_next == dev->queue_size) + return NULL; + + return &dev->queue[dev->queue_next]; +} + +static inline int +queue_set_num_elements(struct libevdev *dev, size_t nelem) +{ + if (nelem > dev->queue_size) + return 1; + + dev->queue_next = nelem; + + return 0; +} + +#define max_mask(uc, lc) \ + case EV_##uc: \ + *mask = dev->lc##_bits; \ + max = libevdev_event_type_get_max(type); \ + break; + +static inline int +type_to_mask_const(const struct libevdev *dev, unsigned int type, const unsigned long **mask) +{ + int max; + + switch(type) { + max_mask(ABS, abs); + max_mask(REL, rel); + max_mask(KEY, key); + max_mask(LED, led); + max_mask(MSC, msc); + max_mask(SW, sw); + max_mask(FF, ff); + max_mask(REP, rep); + max_mask(SND, snd); + default: + max = -1; + break; + } + + return max; +} + +static inline int +type_to_mask(struct libevdev *dev, unsigned int type, unsigned long **mask) +{ + int max; + + switch(type) { + max_mask(ABS, abs); + max_mask(REL, rel); + max_mask(KEY, key); + max_mask(LED, led); + max_mask(MSC, msc); + max_mask(SW, sw); + max_mask(FF, ff); + max_mask(REP, rep); + max_mask(SND, snd); + default: + max = -1; + break; + } + + return max; +} + +#undef max_mask +#endif diff --git a/nativelib/src/main/cpp/libevdev/libevdev-names.c b/nativelib/src/main/cpp/libevdev/libevdev-names.c new file mode 100644 index 0000000000..f8c991b84a --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev-names.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 David Herrmann + */ + +#include +#include +#include +#include + +#include "libevdev-int.h" +#include "libevdev-util.h" +#include "libevdev.h" + +#include "event-names.h" + +struct name_lookup { + const char *name; + size_t len; +}; + +static int cmp_entry(const void *vlookup, const void *ventry) +{ + const struct name_lookup *lookup = vlookup; + const struct name_entry *entry = ventry; + int r; + + r = strncmp(lookup->name, entry->name, lookup->len); + if (!r) { + if (entry->name[lookup->len]) + r = -1; + else + r = 0; + } + + return r; +} + +static const struct name_entry* +lookup_name(const struct name_entry *array, size_t asize, + struct name_lookup *lookup) +{ + const struct name_entry *entry; + + entry = bsearch(lookup, array, asize, sizeof(*array), cmp_entry); + if (!entry) + return NULL; + + return entry; +} + +LIBEVDEV_EXPORT int +libevdev_event_type_from_name(const char *name) +{ + return libevdev_event_type_from_name_n(name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_type_from_name_n(const char *name, size_t len) +{ + struct name_lookup lookup; + const struct name_entry *entry; + + lookup.name = name; + lookup.len = len; + + entry = lookup_name(ev_names, ARRAY_LENGTH(ev_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +static int type_from_prefix(const char *name, ssize_t len) +{ + const char *e; + size_t i; + ssize_t l; + + /* MAX_ is not allowed, even though EV_MAX exists */ + if (startswith(name, len, "MAX_", 4)) + return -1; + /* BTN_ is special as there is no EV_BTN type */ + if (startswith(name, len, "BTN_", 4)) + return EV_KEY; + /* FF_STATUS_ is special as FF_ is a prefix of it, so test it first */ + if (startswith(name, len, "FF_STATUS_", 10)) + return EV_FF_STATUS; + + for (i = 0; i < ARRAY_LENGTH(ev_names); ++i) { + /* skip EV_ prefix so @e is suffix of [EV_]XYZ */ + e = &ev_names[i].name[3]; + l = strlen(e); + + /* compare prefix and test for trailing _ */ + if (len > l && startswith(name, len, e, l) && name[l] == '_') + return ev_names[i].value; + } + + return -1; +} + +LIBEVDEV_EXPORT int +libevdev_event_code_from_name(unsigned int type, const char *name) +{ + return libevdev_event_code_from_name_n(type, name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_code_from_name_n(unsigned int type, const char *name, size_t len) +{ + struct name_lookup lookup; + const struct name_entry *entry; + int real_type; + + /* verify that @name is really of type @type */ + real_type = type_from_prefix(name, len); + if (real_type < 0 || (unsigned int)real_type != type) + return -1; + + /* now look up the name @name and return the constant */ + lookup.name = name; + lookup.len = len; + + entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +LIBEVDEV_EXPORT int +libevdev_event_value_from_name(unsigned int type, unsigned int code, const char *name) +{ + return libevdev_event_value_from_name_n(type, code, name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_value_from_name_n(unsigned int type, unsigned int code, const char *name, size_t len) +{ + struct name_lookup lookup; + const struct name_entry *entry; + + if (type != EV_ABS || code != ABS_MT_TOOL_TYPE) + return -1; + + lookup.name = name; + lookup.len = len; + + entry = lookup_name(tool_type_names, ARRAY_LENGTH(tool_type_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +LIBEVDEV_EXPORT int +libevdev_property_from_name(const char *name) +{ + return libevdev_property_from_name_n(name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_property_from_name_n(const char *name, size_t len) +{ + struct name_lookup lookup; + const struct name_entry *entry; + + lookup.name = name; + lookup.len = len; + + entry = lookup_name(prop_names, ARRAY_LENGTH(prop_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +LIBEVDEV_EXPORT int +libevdev_event_code_from_code_name(const char *name) +{ + return libevdev_event_code_from_code_name_n(name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_code_from_code_name_n(const char *name, size_t len) +{ + const struct name_entry *entry; + struct name_lookup lookup; + + /* now look up the name @name and return the constant */ + lookup.name = name; + lookup.len = len; + + entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +LIBEVDEV_EXPORT int +libevdev_event_type_from_code_name(const char *name) +{ + return libevdev_event_type_from_code_name_n(name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_type_from_code_name_n(const char *name, size_t len) +{ + const struct name_entry *entry; + struct name_lookup lookup; + + /* First look up if the name exists, we dont' want to return a valid + * type for an invalid code name */ + lookup.name = name; + lookup.len = len; + + entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup); + + return entry ? type_from_prefix(name, len) : -1; +} diff --git a/nativelib/src/main/cpp/libevdev/libevdev-uinput-int.h b/nativelib/src/main/cpp/libevdev/libevdev-uinput-int.h new file mode 100644 index 0000000000..d7e674a93d --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev-uinput-int.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +struct libevdev_uinput { + int fd; /**< file descriptor to uinput */ + int fd_is_managed; /**< do we need to close it? */ + char *name; /**< device name */ + char *syspath; /**< /sys path */ + char *devnode; /**< device node */ + time_t ctime[2]; /**< before/after UI_DEV_CREATE */ +}; diff --git a/nativelib/src/main/cpp/libevdev/libevdev-uinput.c b/nativelib/src/main/cpp/libevdev/libevdev-uinput.c new file mode 100644 index 0000000000..b2f6144c6b --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev-uinput.c @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libevdev-int.h" +#include "libevdev-uinput-int.h" +#include "libevdev-uinput.h" +#include "libevdev-util.h" +#include "libevdev.h" + +#ifndef UINPUT_IOCTL_BASE +#define UINPUT_IOCTL_BASE 'U' +#endif + +#ifndef UI_SET_PROPBIT +#define UI_SET_PROPBIT _IOW(UINPUT_IOCTL_BASE, 110, int) +#endif + +static struct libevdev_uinput * +alloc_uinput_device(const char *name) +{ + struct libevdev_uinput *uinput_dev; + + uinput_dev = calloc(1, sizeof(struct libevdev_uinput)); + if (uinput_dev) { + uinput_dev->name = strdup(name); + uinput_dev->fd = -1; + } + + return uinput_dev; +} + +static inline int +set_abs(const struct libevdev *dev, int fd, unsigned int code) +{ + const struct input_absinfo *abs = libevdev_get_abs_info(dev, code); + struct uinput_abs_setup abs_setup = {0}; + int rc; + + abs_setup.code = code; + abs_setup.absinfo = *abs; + rc = ioctl(fd, UI_ABS_SETUP, &abs_setup); + return rc; +} + +static int +set_evbits(const struct libevdev *dev, int fd, struct uinput_user_dev *uidev) +{ + int rc = 0; + unsigned int type; + + for (type = 0; type < EV_CNT; type++) { + unsigned int code; + int max; + int uinput_bit; + const unsigned long *mask; + + if (!libevdev_has_event_type(dev, type)) + continue; + + rc = ioctl(fd, UI_SET_EVBIT, type); + if (rc == -1) + break; + + /* uinput can't set EV_REP */ + if (type == EV_REP) + continue; + + max = type_to_mask_const(dev, type, &mask); + if (max == -1) + continue; + + switch(type) { + case EV_KEY: uinput_bit = UI_SET_KEYBIT; break; + case EV_REL: uinput_bit = UI_SET_RELBIT; break; + case EV_ABS: uinput_bit = UI_SET_ABSBIT; break; + case EV_MSC: uinput_bit = UI_SET_MSCBIT; break; + case EV_LED: uinput_bit = UI_SET_LEDBIT; break; + case EV_SND: uinput_bit = UI_SET_SNDBIT; break; + case EV_FF: uinput_bit = UI_SET_FFBIT; break; + case EV_SW: uinput_bit = UI_SET_SWBIT; break; + default: + rc = -1; + errno = EINVAL; + goto out; + } + + for (code = 0; code <= (unsigned int)max; code++) { + if (!libevdev_has_event_code(dev, type, code)) + continue; + + rc = ioctl(fd, uinput_bit, code); + if (rc == -1) + goto out; + + if (type == EV_ABS) { + if (uidev == NULL) { + rc = set_abs(dev, fd, code); + if (rc != 0) + goto out; + } else { + const struct input_absinfo *abs = + libevdev_get_abs_info(dev, code); + + uidev->absmin[code] = abs->minimum; + uidev->absmax[code] = abs->maximum; + uidev->absfuzz[code] = abs->fuzz; + uidev->absflat[code] = abs->flat; + /* uinput has no resolution in the + * device struct */ + } + } + } + + } + +out: + return rc; +} + +static int +set_props(const struct libevdev *dev, int fd) +{ + unsigned int prop; + int rc = 0; + + for (prop = 0; prop <= INPUT_PROP_MAX; prop++) { + if (!libevdev_has_property(dev, prop)) + continue; + + rc = ioctl(fd, UI_SET_PROPBIT, prop); + if (rc == -1) { + /* If UI_SET_PROPBIT is not supported, treat -EINVAL + * as success. The kernel only sends -EINVAL for an + * invalid ioctl, invalid INPUT_PROP_MAX or if the + * ioctl is called on an already created device. The + * last two can't happen here. + */ + if (errno == EINVAL) + rc = 0; + break; + } + } + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_uinput_get_fd(const struct libevdev_uinput *uinput_dev) +{ + return uinput_dev->fd; +} + +#ifdef __FreeBSD__ +/* + * FreeBSD does not have anything similar to sysfs. + * Set libevdev_uinput->syspath to NULL unconditionally. + * Look up the device nodes directly instead of via sysfs, as this matches what + * is returned by the UI_GET_SYSNAME ioctl() on FreeBSD. + */ +static int +fetch_syspath_and_devnode(struct libevdev_uinput *uinput_dev) +{ +#define DEV_INPUT_DIR "/dev/input/" + int rc; + char buf[sizeof(DEV_INPUT_DIR) + 64] = DEV_INPUT_DIR; + + rc = ioctl(uinput_dev->fd, + UI_GET_SYSNAME(sizeof(buf) - strlen(DEV_INPUT_DIR)), + &buf[strlen(DEV_INPUT_DIR)]); + if (rc == -1) + return -1; + + uinput_dev->syspath = NULL; + uinput_dev->devnode = strdup(buf); + + return 0; +#undef DEV_INPUT_DIR +} + +#else /* !__FreeBSD__ */ + +static int is_event_device(const struct dirent *dent) { + return strncmp("event", dent->d_name, 5) == 0; +} + +static char * +fetch_device_node(const char *path) +{ + char *devnode = NULL; + struct dirent **namelist; + int ndev, i; + + ndev = scandir(path, &namelist, is_event_device, alphasort); + if (ndev <= 0) + return NULL; + + /* ndev should only ever be 1 */ + + for (i = 0; i < ndev; i++) { + if (!devnode && asprintf(&devnode, "/dev/input/%s", namelist[i]->d_name) == -1) + devnode = NULL; + free(namelist[i]); + } + + free(namelist); + + return devnode; +} + +static int is_input_device(const struct dirent *dent) { + return strncmp("input", dent->d_name, 5) == 0; +} + +static int +fetch_syspath_and_devnode(struct libevdev_uinput *uinput_dev) +{ +#define SYS_INPUT_DIR "/sys/devices/virtual/input/" + struct dirent **namelist; + int ndev, i; + int rc; + char buf[sizeof(SYS_INPUT_DIR) + 64] = SYS_INPUT_DIR; + + rc = ioctl(uinput_dev->fd, + UI_GET_SYSNAME(sizeof(buf) - strlen(SYS_INPUT_DIR)), + &buf[strlen(SYS_INPUT_DIR)]); + if (rc != -1) { + uinput_dev->syspath = strdup(buf); + uinput_dev->devnode = fetch_device_node(buf); + return 0; + } + + ndev = scandir(SYS_INPUT_DIR, &namelist, is_input_device, alphasort); + if (ndev <= 0) + return -1; + + for (i = 0; i < ndev; i++) { + int fd, len; + struct stat st; + + rc = snprintf(buf, sizeof(buf), "%s%s/name", + SYS_INPUT_DIR, + namelist[i]->d_name); + if (rc < 0 || (size_t)rc >= sizeof(buf)) { + continue; + } + + /* created within time frame */ + fd = open(buf, O_RDONLY); + if (fd < 0) + continue; + + /* created before UI_DEV_CREATE, or after it finished */ + if (fstat(fd, &st) == -1 || + st.st_ctime < uinput_dev->ctime[0] || + st.st_ctime > uinput_dev->ctime[1]) { + close(fd); + continue; + } + + len = read(fd, buf, sizeof(buf)); + close(fd); + if (len <= 0) + continue; + + buf[len - 1] = '\0'; /* file contains \n */ + if (strcmp(buf, uinput_dev->name) == 0) { + if (uinput_dev->syspath) { + /* FIXME: could descend into bit comparison here */ + log_info(NULL, "multiple identical devices found. syspath is unreliable\n"); + break; + } + + rc = snprintf(buf, sizeof(buf), "%s%s", + SYS_INPUT_DIR, + namelist[i]->d_name); + + if (rc < 0 || (size_t)rc >= sizeof(buf)) { + log_error(NULL, "Invalid syspath, syspath is unreliable\n"); + break; + } + + uinput_dev->syspath = strdup(buf); + uinput_dev->devnode = fetch_device_node(buf); + } + } + + for (i = 0; i < ndev; i++) + free(namelist[i]); + free(namelist); + + return uinput_dev->devnode ? 0 : -1; +#undef SYS_INPUT_DIR +} +#endif /* __FreeBSD__*/ + +static int +uinput_create_write(const struct libevdev *dev, int fd) +{ + int rc; + struct uinput_user_dev uidev; + + memset(&uidev, 0, sizeof(uidev)); + + strncpy(uidev.name, libevdev_get_name(dev), UINPUT_MAX_NAME_SIZE - 1); + uidev.id.vendor = libevdev_get_id_vendor(dev); + uidev.id.product = libevdev_get_id_product(dev); + uidev.id.bustype = libevdev_get_id_bustype(dev); + uidev.id.version = libevdev_get_id_version(dev); + + if (set_evbits(dev, fd, &uidev) != 0) + goto error; + if (set_props(dev, fd) != 0) + goto error; + + rc = write(fd, &uidev, sizeof(uidev)); + if (rc < 0) { + goto error; + } else if ((size_t)rc < sizeof(uidev)) { + errno = EINVAL; + goto error; + } + + errno = 0; + +error: + return -errno; +} + +static int +uinput_create_DEV_SETUP(const struct libevdev *dev, int fd, + struct libevdev_uinput *new_device) +{ + int rc; + struct uinput_setup setup; + + if (set_evbits(dev, fd, NULL) != 0) + goto error; + if (set_props(dev, fd) != 0) + goto error; + + memset(&setup, 0, sizeof(setup)); + strncpy(setup.name, libevdev_get_name(dev), UINPUT_MAX_NAME_SIZE - 1); + setup.id.vendor = libevdev_get_id_vendor(dev); + setup.id.product = libevdev_get_id_product(dev); + setup.id.bustype = libevdev_get_id_bustype(dev); + setup.id.version = libevdev_get_id_version(dev); + setup.ff_effects_max = libevdev_has_event_type(dev, EV_FF) ? 10 : 0; + + rc = ioctl(fd, UI_DEV_SETUP, &setup); + if (rc == 0) + errno = 0; +error: + return -errno; +} + +LIBEVDEV_EXPORT int +libevdev_uinput_create_from_device(const struct libevdev *dev, int fd, struct libevdev_uinput** uinput_dev) +{ + int rc; + struct libevdev_uinput *new_device; + int close_fd_on_error = (fd == LIBEVDEV_UINPUT_OPEN_MANAGED); + unsigned int uinput_version = 0; + + new_device = alloc_uinput_device(libevdev_get_name(dev)); + if (!new_device) + return -ENOMEM; + + if (fd == LIBEVDEV_UINPUT_OPEN_MANAGED) { + fd = open("/dev/uinput", O_RDWR|O_CLOEXEC); + if (fd < 0) + goto error; + + new_device->fd_is_managed = 1; + } else if (fd < 0) { + log_bug(NULL, "Invalid fd %d\n", fd); + errno = EBADF; + goto error; + } + + if (ioctl(fd, UI_GET_VERSION, &uinput_version) == 0 && + uinput_version >= 5) + rc = uinput_create_DEV_SETUP(dev, fd, new_device); + else + rc = uinput_create_write(dev, fd); + + if (rc != 0) + goto error; + + /* ctime notes time before/after ioctl to help us filter out devices + when traversing /sys/devices/virtual/input to find the device + node. + + this is in seconds, so ctime[0]/[1] will almost always be + identical but /sys doesn't give us sub-second ctime so... + */ + new_device->ctime[0] = time(NULL); + + rc = ioctl(fd, UI_DEV_CREATE, NULL); + if (rc == -1) + goto error; + + new_device->ctime[1] = time(NULL); + new_device->fd = fd; + + if (fetch_syspath_and_devnode(new_device) == -1) { + log_error(NULL, "unable to fetch syspath or device node.\n"); + errno = ENODEV; + goto error; + } + + *uinput_dev = new_device; + + return 0; + +error: + rc = -errno; + libevdev_uinput_destroy(new_device); + if (fd != -1 && close_fd_on_error) + close(fd); + return rc; +} + +LIBEVDEV_EXPORT void +libevdev_uinput_destroy(struct libevdev_uinput *uinput_dev) +{ + if (!uinput_dev) + return; + + if (uinput_dev->fd >= 0) { + (void)ioctl(uinput_dev->fd, UI_DEV_DESTROY, NULL); + if (uinput_dev->fd_is_managed) + close(uinput_dev->fd); + } + free(uinput_dev->syspath); + free(uinput_dev->devnode); + free(uinput_dev->name); + free(uinput_dev); +} + +LIBEVDEV_EXPORT const char* +libevdev_uinput_get_syspath(struct libevdev_uinput *uinput_dev) +{ + return uinput_dev->syspath; +} + +LIBEVDEV_EXPORT const char* +libevdev_uinput_get_devnode(struct libevdev_uinput *uinput_dev) +{ + return uinput_dev->devnode; +} + +LIBEVDEV_EXPORT int +libevdev_uinput_write_event(const struct libevdev_uinput *uinput_dev, + unsigned int type, + unsigned int code, + int value) +{ + struct input_event ev = { + .input_event_sec = 0, + .input_event_usec = 0, + .type = type, + .code = code, + .value = value + }; + int fd = libevdev_uinput_get_fd(uinput_dev); + int rc, max; + + if (type > EV_MAX) + return -EINVAL; + + max = libevdev_event_type_get_max(type); + if (max == -1 || code > (unsigned int)max) + return -EINVAL; + + rc = write(fd, &ev, sizeof(ev)); + + return rc < 0 ? -errno : 0; +} diff --git a/nativelib/src/main/cpp/libevdev/libevdev-uinput.h b/nativelib/src/main/cpp/libevdev/libevdev-uinput.h new file mode 100644 index 0000000000..b317fe7024 --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev-uinput.h @@ -0,0 +1,254 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef LIBEVDEV_UINPUT_H +#define LIBEVDEV_UINPUT_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct libevdev_uinput; + +/** + * @defgroup uinput uinput device creation + * + * Creation of uinput devices based on existing libevdev devices. These functions + * help to create uinput devices that emulate libevdev devices. In the simplest + * form it serves to duplicate an existing device: + * + * @code + * int err; + * int fd, uifd; + * struct libevdev *dev; + * struct libevdev_uinput *uidev; + * + * fd = open("/dev/input/event0", O_RDONLY); + * if (fd < 0) + * return -errno; + * + * err = libevdev_new_from_fd(fd, &dev); + * if (err != 0) + * return err; + * + * uifd = open("/dev/uinput", O_RDWR); + * if (uifd < 0) + * return -errno; + * + * err = libevdev_uinput_create_from_device(dev, uifd, &uidev); + * if (err != 0) + * return err; + * + * // post a REL_X event + * err = libevdev_uinput_write_event(uidev, EV_REL, REL_X, -1); + * if (err != 0) + * return err; + * err = libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); + * if (err != 0) + * return err; + * + * libevdev_uinput_destroy(uidev); + * libevdev_free(dev); + * close(uifd); + * close(fd); + * + * @endcode + * + * Alternatively, a device can be constructed from scratch: + * + * @code + * int err; + * struct libevdev *dev; + * struct libevdev_uinput *uidev; + * + * dev = libevdev_new(); + * libevdev_set_name(dev, "test device"); + * libevdev_enable_event_type(dev, EV_REL); + * libevdev_enable_event_code(dev, EV_REL, REL_X, NULL); + * libevdev_enable_event_code(dev, EV_REL, REL_Y, NULL); + * libevdev_enable_event_type(dev, EV_KEY); + * libevdev_enable_event_code(dev, EV_KEY, BTN_LEFT, NULL); + * libevdev_enable_event_code(dev, EV_KEY, BTN_MIDDLE, NULL); + * libevdev_enable_event_code(dev, EV_KEY, BTN_RIGHT, NULL); + * + * err = libevdev_uinput_create_from_device(dev, + * LIBEVDEV_UINPUT_OPEN_MANAGED, + * &uidev); + * if (err != 0) + * return err; + * + * // ... do something ... + * + * libevdev_uinput_destroy(uidev); + * + * @endcode + */ + +enum libevdev_uinput_open_mode { + /* intentionally -2 to avoid code like below from accidentally working: + fd = open("/dev/uinput", O_RDWR); // fails, fd is -1 + libevdev_uinput_create_from_device(dev, fd, &uidev); // may hide the error */ + LIBEVDEV_UINPUT_OPEN_MANAGED = -2 /**< let libevdev open and close @c /dev/uinput */ +}; + +/** + * @ingroup uinput + * + * Create a uinput device based on the given libevdev device. The uinput device + * will be an exact copy of the libevdev device, minus the bits that uinput doesn't + * allow to be set. + * + * If uinput_fd is @ref LIBEVDEV_UINPUT_OPEN_MANAGED, libevdev_uinput_create_from_device() + * will open @c /dev/uinput in read/write mode and manage the file descriptor. + * Otherwise, uinput_fd must be opened by the caller and opened with the + * appropriate permissions. + * + * The device's lifetime is tied to the uinput file descriptor, closing it will + * destroy the uinput device. You should call libevdev_uinput_destroy() before + * closing the file descriptor to free allocated resources. + * A file descriptor can only create one uinput device at a time; the second device + * will fail with -EINVAL. + * + * You don't need to keep the file descriptor variable around, + * libevdev_uinput_get_fd() will return it when needed. + * + * @note Due to limitations in the uinput kernel module, REP_DELAY and + * REP_PERIOD will default to the kernel defaults, not to the ones set in the + * source device. + * + * @note On FreeBSD, if the UI_GET_SYSNAME ioctl() fails, there is no other way + * to get a device, and the function call will fail. + * + * @param dev The device to duplicate + * @param uinput_fd @ref LIBEVDEV_UINPUT_OPEN_MANAGED or a file descriptor to @c /dev/uinput, + * @param[out] uinput_dev The newly created libevdev device. + * + * @return 0 on success or a negative errno on failure. On failure, the value of + * uinput_dev is unmodified. + * + * @see libevdev_uinput_destroy + */ +int libevdev_uinput_create_from_device(const struct libevdev *dev, + int uinput_fd, + struct libevdev_uinput **uinput_dev); + +/** + * @ingroup uinput + * + * Destroy a previously created uinput device and free associated memory. + * + * If the device was opened with @ref LIBEVDEV_UINPUT_OPEN_MANAGED, + * libevdev_uinput_destroy() also closes the file descriptor. Otherwise, the + * fd is left as-is and must be closed by the caller. + * + * @param uinput_dev A previously created uinput device. + */ +void libevdev_uinput_destroy(struct libevdev_uinput *uinput_dev); + +/** + * @ingroup uinput + * + * Return the file descriptor used to create this uinput device. This is the + * fd pointing to /dev/uinput. This file descriptor may be used to write + * events that are emitted by the uinput device. + * Closing this file descriptor will destroy the uinput device, you should + * call libevdev_uinput_destroy() first to free allocated resources. + * + * @param uinput_dev A previously created uinput device. + * + * @return The file descriptor used to create this device + */ +int libevdev_uinput_get_fd(const struct libevdev_uinput *uinput_dev); + +/** + * @ingroup uinput + * + * Return the syspath representing this uinput device. If the UI_GET_SYSNAME + * ioctl is not available, libevdev makes an educated guess. + * The UI_GET_SYSNAME ioctl is available since Linux 3.15. + * + * The syspath returned is the one of the input node itself + * (e.g. /sys/devices/virtual/input/input123), not the syspath of the device + * node returned with libevdev_uinput_get_devnode(). + * + * @note This function may return NULL if UI_GET_SYSNAME is not available. + * In that case, libevdev uses ctime and the device name to guess devices. + * To avoid false positives, wait at least 1.5s between creating devices that + * have the same name. + * + * @note FreeBSD does not have sysfs, on FreeBSD this function always returns + * NULL. + * + * @param uinput_dev A previously created uinput device. + * @return The syspath for this device, including the preceding /sys + * + * @see libevdev_uinput_get_devnode + */ +const char* libevdev_uinput_get_syspath(struct libevdev_uinput *uinput_dev); + +/** + * @ingroup uinput + * + * Return the device node representing this uinput device. + * + * This relies on libevdev_uinput_get_syspath() to provide a valid syspath. + * See libevdev_uinput_get_syspath() for more details. + * + * @note This function may return NULL. libevdev may have to guess the + * syspath and the device node. See libevdev_uinput_get_syspath() for details. + * + * @note On FreeBSD, this function can not return NULL. libudev uses the + * UI_GET_SYSNAME ioctl to get the device node on this platform and if that + * fails, the call to libevdev_uinput_create_from_device() fails. + * + * @param uinput_dev A previously created uinput device. + * @return The device node for this device, in the form of /dev/input/eventN + * + * @see libevdev_uinput_get_syspath + */ +const char* libevdev_uinput_get_devnode(struct libevdev_uinput *uinput_dev); + +/** + * @ingroup uinput + * + * Post an event through the uinput device. It is the caller's responsibility + * that any event sequence is terminated with an EV_SYN/SYN_REPORT/0 event. + * Otherwise, listeners on the device node will not see the events until the + * next EV_SYN event is posted. + * + * @param uinput_dev A previously created uinput device. + * @param type Event type (EV_ABS, EV_REL, etc.) + * @param code Event code (ABS_X, REL_Y, etc.) + * @param value The event value + * @return 0 on success or a negative errno on error + */ +int libevdev_uinput_write_event(const struct libevdev_uinput *uinput_dev, + unsigned int type, + unsigned int code, + int value); +#ifdef __cplusplus +} +#endif + +#endif /* LIBEVDEV_UINPUT_H */ diff --git a/nativelib/src/main/cpp/libevdev/libevdev-util.h b/nativelib/src/main/cpp/libevdev/libevdev-util.h new file mode 100644 index 0000000000..f2ad9a2d43 --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev-util.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +#ifndef _UTIL_H_ +#define _UTIL_H_ + +#include +#include + +#define LONG_BITS (sizeof(long) * 8) +#define NLONGS(x) (((x) + LONG_BITS - 1) / LONG_BITS) +#define ARRAY_LENGTH(a) (sizeof(a) / (sizeof((a)[0]))) +#define unlikely(x) (__builtin_expect(!!(x),0)) + +#undef min +#undef max +#ifdef __GNUC__ +#define min(a,b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _b : _a; \ + }) +#define max(a,b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _a : _b; \ + }) +#else +#define min(a,b) ((a) > (b) ? (b) : (a)) +#define max(a,b) ((a) > (b) ? (a) : (b)) +#endif + +static inline bool +startswith(const char *str, size_t len, const char *prefix, size_t plen) +{ + return len >= plen && !strncmp(str, prefix, plen); +} + +static inline int +bit_is_set(const unsigned long *array, int bit) +{ + return !!(array[bit / LONG_BITS] & (1LL << (bit % LONG_BITS))); +} + +static inline void +set_bit(unsigned long *array, int bit) +{ + array[bit / LONG_BITS] |= (1LL << (bit % LONG_BITS)); +} + +static inline void +clear_bit(unsigned long *array, int bit) +{ + array[bit / LONG_BITS] &= ~(1LL << (bit % LONG_BITS)); +} + +static inline void +set_bit_state(unsigned long *array, int bit, int state) +{ + if (state) + set_bit(array, bit); + else + clear_bit(array, bit); +} + +#endif diff --git a/nativelib/src/main/cpp/libevdev/libevdev.c b/nativelib/src/main/cpp/libevdev/libevdev.c new file mode 100644 index 0000000000..743fc02a1d --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev.c @@ -0,0 +1,1882 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libevdev-int.h" +#include "libevdev-util.h" +#include "libevdev.h" + +#include "event-names.h" + +#define MAXEVENTS 64 + +enum event_filter_status { + EVENT_FILTER_NONE, /**< Event untouched by filters */ + EVENT_FILTER_MODIFIED, /**< Event was modified */ + EVENT_FILTER_DISCARD, /**< Discard current event */ +}; + +/* Keeps a record of touches during SYN_DROPPED */ +enum touch_state { + TOUCH_OFF, + TOUCH_STARTED, /* Started during SYN_DROPPED */ + TOUCH_STOPPED, /* Stopped during SYN_DROPPED */ + TOUCH_ONGOING, /* Existed before, still have same tracking ID */ + TOUCH_CHANGED, /* Existed before but have new tracking ID now, so + stopped + started in that slot */ +}; + +struct slot_change_state { + enum touch_state state; + unsigned long axes[NLONGS(ABS_CNT)]; /* bitmask for updated axes */ +}; + +static int sync_mt_state(struct libevdev *dev, + struct slot_change_state changes_out[dev->num_slots]); + +static int +update_key_state(struct libevdev *dev, const struct input_event *e); + +static inline int* +slot_value(const struct libevdev *dev, int slot, int axis) +{ + if (unlikely(slot > dev->num_slots)) { + log_bug(dev, "Slot %d exceeds number of slots (%d)\n", slot, dev->num_slots); + slot = 0; + } + if (unlikely(axis < ABS_MT_MIN || axis > ABS_MT_MAX)) { + log_bug(dev, "MT axis %d is outside the valid range [%d,%d]\n", + axis, ABS_MT_MIN, ABS_MT_MAX); + axis = ABS_MT_MIN; + } + return &dev->mt_slot_vals[slot * ABS_MT_CNT + axis - ABS_MT_MIN]; +} + +static int +init_event_queue(struct libevdev *dev) +{ + const int MIN_QUEUE_SIZE = 256; + int nevents = 1; /* terminating SYN_REPORT */ + int nslots; + unsigned int type, code; + + /* count the number of axes, keys, etc. to get a better idea at how + many events per EV_SYN we could possibly get. That's the max we + may get during SYN_DROPPED too. Use double that, just so we have + room for events while syncing a device. + */ + for (type = EV_KEY; type < EV_MAX; type++) { + int max = libevdev_event_type_get_max(type); + for (code = 0; max > 0 && code < (unsigned int) max; code++) { + if (libevdev_has_event_code(dev, type, code)) + nevents++; + } + } + + nslots = libevdev_get_num_slots(dev); + if (nslots > 1) { + int num_mt_axes = 0; + + for (code = ABS_MT_SLOT; code <= ABS_MAX; code++) { + if (libevdev_has_event_code(dev, EV_ABS, code)) + num_mt_axes++; + } + + /* We already counted the first slot in the initial count */ + nevents += num_mt_axes * (nslots - 1); + } + + return queue_alloc(dev, max(MIN_QUEUE_SIZE, nevents * 2)); +} + +static void +libevdev_dflt_log_func(enum libevdev_log_priority priority, + void *data, + const char *file, int line, const char *func, + const char *format, va_list args) +{ + const char *prefix; + switch(priority) { + case LIBEVDEV_LOG_ERROR: prefix = "libevdev error"; break; + case LIBEVDEV_LOG_INFO: prefix = "libevdev info"; break; + case LIBEVDEV_LOG_DEBUG: + prefix = "libevdev debug"; + break; + default: + prefix = "libevdev INVALID LOG PRIORITY"; + break; + } + /* default logging format: + libevev error in libevdev_some_func: blah blah + libevev info in libevdev_some_func: blah blah + libevev debug in file.c:123:libevdev_some_func: blah blah + */ + + fprintf(stderr, "%s in ", prefix); + if (priority == LIBEVDEV_LOG_DEBUG) + fprintf(stderr, "%s:%d:", file, line); + fprintf(stderr, "%s: ", func); + vfprintf(stderr, format, args); +} + +static void +fix_invalid_absinfo(const struct libevdev *dev, + int axis, + struct input_absinfo* abs_info) +{ + /* + * The reported absinfo for ABS_MT_TRACKING_ID is sometimes + * uninitialized for certain mtk-soc, due to init code mangling + * in the vendor kernel. + */ + if (axis == ABS_MT_TRACKING_ID && + abs_info->maximum == abs_info->minimum) { + abs_info->minimum = -1; + abs_info->maximum = 0xFFFF; + log_bug(dev, + "Device \"%s\" has invalid ABS_MT_TRACKING_ID range", + dev->name); + } +} + +/* + * Global logging settings. + */ +static struct logdata log_data = { + .priority = LIBEVDEV_LOG_INFO, + .global_handler = libevdev_dflt_log_func, + .userdata = NULL, +}; + +void +_libevdev_log_msg(const struct libevdev *dev, + enum libevdev_log_priority priority, + const char *file, int line, const char *func, + const char *format, ...) +{ + va_list args; + + if (dev && dev->log.device_handler) { + /** + * if both global handler and device handler are set + * we've set up the handlers wrong. And that means we'll + * likely get the printf args wrong and cause all sorts of + * mayhem. Seppuku is called for. + */ + if (unlikely(dev->log.global_handler)) + abort(); + + if (priority > dev->log.priority) + return; + } else if (!log_data.global_handler || priority > log_data.priority) { + return; + } else if (unlikely(log_data.device_handler)) { + abort(); /* Seppuku, see above */ + } + + va_start(args, format); + if (dev && dev->log.device_handler) + dev->log.device_handler(dev, priority, dev->log.userdata, file, line, func, format, args); + else + log_data.global_handler(priority, log_data.userdata, file, line, func, format, args); + va_end(args); +} + +static void +libevdev_reset(struct libevdev *dev) +{ + enum libevdev_log_priority pri = dev->log.priority; + libevdev_device_log_func_t handler = dev->log.device_handler; + + free(dev->name); + free(dev->phys); + free(dev->uniq); + free(dev->mt_slot_vals); + memset(dev, 0, sizeof(*dev)); + dev->fd = -1; + dev->initialized = false; + dev->num_slots = -1; + dev->current_slot = -1; + dev->grabbed = LIBEVDEV_UNGRAB; + dev->sync_state = SYNC_NONE; + dev->log.priority = pri; + dev->log.device_handler = handler; + libevdev_enable_event_type(dev, EV_SYN); +} + +LIBEVDEV_EXPORT struct libevdev* +libevdev_new(void) +{ + struct libevdev *dev; + + dev = calloc(1, sizeof(*dev)); + if (!dev) + return NULL; + + libevdev_reset(dev); + + return dev; +} + +LIBEVDEV_EXPORT int +libevdev_new_from_fd(int fd, struct libevdev **dev) +{ + struct libevdev *d; + int rc; + + d = libevdev_new(); + if (!d) + return -ENOMEM; + + rc = libevdev_set_fd(d, fd); + if (rc < 0) + libevdev_free(d); + else + *dev = d; + return rc; +} + +LIBEVDEV_EXPORT void +libevdev_free(struct libevdev *dev) +{ + if (!dev) + return; + + queue_free(dev); + libevdev_reset(dev); + free(dev); +} + +LIBEVDEV_EXPORT void +libevdev_set_log_function(libevdev_log_func_t logfunc, void *data) +{ + log_data.global_handler = logfunc; + log_data.userdata = data; +} + +LIBEVDEV_EXPORT void +libevdev_set_log_priority(enum libevdev_log_priority priority) +{ + if (priority > LIBEVDEV_LOG_DEBUG) + priority = LIBEVDEV_LOG_DEBUG; + log_data.priority = priority; +} + +LIBEVDEV_EXPORT enum libevdev_log_priority +libevdev_get_log_priority(void) +{ + return log_data.priority; +} + +LIBEVDEV_EXPORT void +libevdev_set_device_log_function(struct libevdev *dev, + libevdev_device_log_func_t logfunc, + enum libevdev_log_priority priority, + void *data) +{ + if (!dev) { + log_bug(NULL, "device must not be NULL\n"); + return; + } + + dev->log.priority = priority; + dev->log.device_handler = logfunc; + dev->log.userdata = data; +} + +enum libevdev_log_priority +_libevdev_log_priority(const struct libevdev *dev) +{ + if (dev && dev->log.device_handler) + return dev->log.priority; + return libevdev_get_log_priority(); +} + +LIBEVDEV_EXPORT int +libevdev_change_fd(struct libevdev *dev, int fd) +{ + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -1; + } + dev->fd = fd; + dev->grabbed = LIBEVDEV_UNGRAB; + return 0; +} + +static void +reset_tracking_ids(struct libevdev *dev) +{ + if (dev->num_slots == -1 || + !libevdev_has_event_code(dev, EV_ABS, ABS_MT_TRACKING_ID)) + return; + + for (int slot = 0; slot < dev->num_slots; slot++) + libevdev_set_slot_value(dev, slot, ABS_MT_TRACKING_ID, -1); +} + +static inline void +free_slots(struct libevdev *dev) +{ + dev->num_slots = -1; + free(dev->mt_slot_vals); + dev->mt_slot_vals = NULL; +} + +static int +init_slots(struct libevdev *dev) +{ + const struct input_absinfo *abs_info; + int rc = 0; + + free(dev->mt_slot_vals); + dev->mt_slot_vals = NULL; + + /* devices with ABS_RESERVED aren't MT devices, + see the documentation for multitouch-related + functions for more details */ + if (libevdev_has_event_code(dev, EV_ABS, ABS_RESERVED) || + !libevdev_has_event_code(dev, EV_ABS, ABS_MT_SLOT)) { + if (dev->num_slots != -1) { + free_slots(dev); + } + return rc; + } + + abs_info = libevdev_get_abs_info(dev, ABS_MT_SLOT); + + free_slots(dev); + dev->num_slots = abs_info->maximum + 1; + dev->mt_slot_vals = calloc(dev->num_slots * ABS_MT_CNT, sizeof(int)); + if (!dev->mt_slot_vals) { + rc = -ENOMEM; + goto out; + } + dev->current_slot = abs_info->value; + + reset_tracking_ids(dev); +out: + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_set_fd(struct libevdev* dev, int fd) +{ + int rc; + int i; + char buf[256]; + + if (dev->initialized) { + log_bug(dev, "device already initialized.\n"); + return -EBADF; + } + + if (fd < 0) { + return -EBADF; + } + + libevdev_reset(dev); + + rc = ioctl(fd, EVIOCGBIT(0, sizeof(dev->bits)), dev->bits); + if (rc < 0) + goto out; + + memset(buf, 0, sizeof(buf)); + rc = ioctl(fd, EVIOCGNAME(sizeof(buf) - 1), buf); + if (rc < 0) + goto out; + + free(dev->name); + dev->name = strdup(buf); + if (!dev->name) { + errno = ENOMEM; + goto out; + } + + free(dev->phys); + dev->phys = NULL; + memset(buf, 0, sizeof(buf)); + rc = ioctl(fd, EVIOCGPHYS(sizeof(buf) - 1), buf); + if (rc < 0) { + /* uinput has no phys */ + if (errno != ENOENT) + goto out; + } else { + dev->phys = strdup(buf); + if (!dev->phys) { + errno = ENOMEM; + goto out; + } + } + + free(dev->uniq); + dev->uniq = NULL; + memset(buf, 0, sizeof(buf)); + rc = ioctl(fd, EVIOCGUNIQ(sizeof(buf) - 1), buf); + if (rc < 0) { + if (errno != ENOENT) + goto out; + } else { + dev->uniq = strdup(buf); + if (!dev->uniq) { + errno = ENOMEM; + goto out; + } + } + + rc = ioctl(fd, EVIOCGID, &dev->ids); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGVERSION, &dev->driver_version); + if (rc < 0) + goto out; + + /* Built on a kernel with props, running against a kernel without property + support. This should not be a fatal case, we'll be missing properties but other + than that everything is as expected. + */ + rc = ioctl(fd, EVIOCGPROP(sizeof(dev->props)), dev->props); + if (rc < 0 && errno != EINVAL) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_REL, sizeof(dev->rel_bits)), dev->rel_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(dev->abs_bits)), dev->abs_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_LED, sizeof(dev->led_bits)), dev->led_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(dev->key_bits)), dev->key_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_SW, sizeof(dev->sw_bits)), dev->sw_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_MSC, sizeof(dev->msc_bits)), dev->msc_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_FF, sizeof(dev->ff_bits)), dev->ff_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_SND, sizeof(dev->snd_bits)), dev->snd_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGKEY(sizeof(dev->key_values)), dev->key_values); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGLED(sizeof(dev->led_values)), dev->led_values); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGSW(sizeof(dev->sw_values)), dev->sw_values); + if (rc < 0) + goto out; + + /* rep is a special case, always set it to 1 for both values if EV_REP is set */ + if (bit_is_set(dev->bits, EV_REP)) { + for (i = 0; i < REP_CNT; i++) + set_bit(dev->rep_bits, i); + rc = ioctl(fd, EVIOCGREP, dev->rep_values); + if (rc < 0) + goto out; + } + + for (i = ABS_X; i <= ABS_MAX; i++) { + if (bit_is_set(dev->abs_bits, i)) { + struct input_absinfo abs_info; + rc = ioctl(fd, EVIOCGABS(i), &abs_info); + if (rc < 0) + goto out; + + fix_invalid_absinfo(dev, i, &abs_info); + + dev->abs_info[i] = abs_info; + } + } + + dev->fd = fd; + + rc = init_slots(dev); + if (rc != 0) + goto out; + + if (dev->num_slots != -1) { + struct slot_change_state unused[dev->num_slots]; + sync_mt_state(dev, unused); + } + + rc = init_event_queue(dev); + if (rc < 0) { + dev->fd = -1; + return -rc; + } + + /* not copying key state because we won't know when we'll start to + * use this fd and key's are likely to change state by then. + * Same with the valuators, really, but they may not change. + */ + + dev->initialized = true; +out: + if (rc) + libevdev_reset(dev); + return rc ? -errno : 0; +} + +LIBEVDEV_EXPORT int +libevdev_get_fd(const struct libevdev* dev) +{ + return dev->fd; +} + +static int +sync_key_state(struct libevdev *dev) +{ + int rc; + int i; + unsigned long keystate[NLONGS(KEY_CNT)] = {0}; + + rc = ioctl(dev->fd, EVIOCGKEY(sizeof(keystate)), keystate); + if (rc < 0) + goto out; + + for (i = 0; i < KEY_CNT; i++) { + int old, new; + old = bit_is_set(dev->key_values, i); + new = bit_is_set(keystate, i); + if (old ^ new) + queue_push_event(dev, EV_KEY, i, new ? 1 : 0); + } + + memcpy(dev->key_values, keystate, rc); + + rc = 0; +out: + return rc ? -errno : 0; +} + +static int +sync_sw_state(struct libevdev *dev) +{ + int rc; + int i; + unsigned long swstate[NLONGS(SW_CNT)] = {0}; + + rc = ioctl(dev->fd, EVIOCGSW(sizeof(swstate)), swstate); + if (rc < 0) + goto out; + + for (i = 0; i < SW_CNT; i++) { + int old, new; + old = bit_is_set(dev->sw_values, i); + new = bit_is_set(swstate, i); + if (old ^ new) + queue_push_event(dev, EV_SW, i, new ? 1 : 0); + } + + memcpy(dev->sw_values, swstate, rc); + + rc = 0; +out: + return rc ? -errno : 0; +} + +static int +sync_led_state(struct libevdev *dev) +{ + int rc; + int i; + unsigned long ledstate[NLONGS(LED_CNT)] = {0}; + + rc = ioctl(dev->fd, EVIOCGLED(sizeof(ledstate)), ledstate); + if (rc < 0) + goto out; + + for (i = 0; i < LED_CNT; i++) { + int old, new; + old = bit_is_set(dev->led_values, i); + new = bit_is_set(ledstate, i); + if (old ^ new) { + queue_push_event(dev, EV_LED, i, new ? 1 : 0); + } + } + + memcpy(dev->led_values, ledstate, rc); + + rc = 0; +out: + return rc ? -errno : 0; +} +static int +sync_abs_state(struct libevdev *dev) +{ + int rc; + int i; + + for (i = ABS_X; i < ABS_CNT; i++) { + struct input_absinfo abs_info; + + if (i >= ABS_MT_MIN && i <= ABS_MT_MAX) + continue; + + if (!bit_is_set(dev->abs_bits, i)) + continue; + + rc = ioctl(dev->fd, EVIOCGABS(i), &abs_info); + if (rc < 0) + goto out; + + if (dev->abs_info[i].value != abs_info.value) { + queue_push_event(dev, EV_ABS, i, abs_info.value); + dev->abs_info[i].value = abs_info.value; + } + } + + rc = 0; +out: + return rc ? -errno : 0; +} + +static int +sync_mt_state(struct libevdev *dev, + struct slot_change_state changes_out[dev->num_slots]) +{ +#define MAX_SLOTS 256 + int rc = 0; + struct slot_change_state changes[MAX_SLOTS] = {0}; + unsigned int nslots = min(MAX_SLOTS, dev->num_slots); + + for (int axis = ABS_MT_MIN; axis <= ABS_MT_MAX; axis++) { + /* EVIOCGMTSLOTS required format */ + struct mt_sync_state { + uint32_t code; + int32_t val[MAX_SLOTS]; + } mt_state; + + if (axis == ABS_MT_SLOT || + !libevdev_has_event_code(dev, EV_ABS, axis)) + continue; + + mt_state.code = axis; + rc = ioctl(dev->fd, EVIOCGMTSLOTS(sizeof(mt_state)), &mt_state); + if (rc < 0) + goto out; + + for (unsigned int slot = 0; slot < nslots; slot++) { + int val_before = *slot_value(dev, slot, axis), + val_after = mt_state.val[slot]; + + if (axis == ABS_MT_TRACKING_ID) { + if (val_before == -1 && val_after != -1) { + changes[slot].state = TOUCH_STARTED; + } else if (val_before != -1 && val_after == -1) { + changes[slot].state = TOUCH_STOPPED; + } else if (val_before != -1 && val_after != -1 && + val_before == val_after) { + changes[slot].state = TOUCH_ONGOING; + } else if (val_before != -1 && val_after != -1 && + val_before != val_after) { + changes[slot].state = TOUCH_CHANGED; + } else { + changes[slot].state = TOUCH_OFF; + } + } + + if (val_before == val_after) + continue; + + *slot_value(dev, slot, axis) = val_after; + + set_bit(changes[slot].axes, axis); + /* note that this slot has updates */ + set_bit(changes[slot].axes, ABS_MT_SLOT); + } + } + + if (dev->num_slots > MAX_SLOTS) + memset(changes_out, 0, sizeof(*changes) * dev->num_slots); + + memcpy(changes_out, changes, sizeof(*changes) * nslots); +out: + return rc; +} + +static void +terminate_slots(struct libevdev *dev, + const struct slot_change_state changes[dev->num_slots], + int *last_reported_slot) +{ + const unsigned int map[] = {BTN_TOOL_FINGER, BTN_TOOL_DOUBLETAP, + BTN_TOOL_TRIPLETAP, BTN_TOOL_QUADTAP, + BTN_TOOL_QUINTTAP}; + bool touches_stopped = false; + int ntouches_before = 0, ntouches_after = 0; + + /* For BTN_TOOL_* emulation, we need to know how many touches we had + * before and how many we have left once we terminate all the ones + * that changed and all the ones that stopped. + */ + for (int slot = 0; slot < dev->num_slots; slot++) { + switch(changes[slot].state) { + case TOUCH_OFF: + break; + case TOUCH_CHANGED: + case TOUCH_STOPPED: + queue_push_event(dev, EV_ABS, ABS_MT_SLOT, slot); + queue_push_event(dev, EV_ABS, ABS_MT_TRACKING_ID, -1); + + *last_reported_slot = slot; + touches_stopped = true; + ntouches_before++; + break; + case TOUCH_ONGOING: + ntouches_before++; + ntouches_after++; + break; + case TOUCH_STARTED: + break; + } + } + + /* If any of the touches stopped, we need to split the sync state + into two frames - one with all the stopped touches, one with the + new touches starting (if any) */ + if (touches_stopped) { + /* Send through the required BTN_TOOL_ 0 and 1 events for + * the previous and current number of fingers. And update + * our own key state accordingly, so that during the second + * sync event frame sync_key_state() sets everything correctly + * for the *real* number of touches. + */ + if (ntouches_before > 0 && ntouches_before <= 5) { + struct input_event ev = { + .type = EV_KEY, + .code = map[ntouches_before - 1], + .value = 0, + }; + queue_push_event(dev, ev.type, ev.code, ev.value); + update_key_state(dev, &ev); + } + + if (ntouches_after > 0 && ntouches_after <= 5) { + struct input_event ev = { + .type = EV_KEY, + .code = map[ntouches_after - 1], + .value = 1, + }; + queue_push_event(dev, ev.type, ev.code, ev.value); + update_key_state(dev, &ev); + } + + queue_push_event(dev, EV_SYN, SYN_REPORT, 0); + } +} + +static int +push_mt_sync_events(struct libevdev *dev, + const struct slot_change_state changes[dev->num_slots], + int last_reported_slot) +{ + struct input_absinfo abs_info; + int rc; + + for (int slot = 0; slot < dev->num_slots; slot++) { + bool have_slot_event = false; + + if (!bit_is_set(changes[slot].axes, ABS_MT_SLOT)) + continue; + + for (int axis = ABS_MT_MIN; axis <= ABS_MT_MAX; axis++) { + if (axis == ABS_MT_SLOT || + !libevdev_has_event_code(dev, EV_ABS, axis)) + continue; + + if (bit_is_set(changes[slot].axes, axis)) { + /* We already sent the tracking id -1 in + * terminate_slots so don't do that again. There + * may be other axes like ABS_MT_TOOL_TYPE that + * need to be synced despite no touch being active */ + if (axis == ABS_MT_TRACKING_ID && + *slot_value(dev, slot, axis) == -1) + continue; + + if (!have_slot_event) { + queue_push_event(dev, EV_ABS, ABS_MT_SLOT, slot); + last_reported_slot = slot; + have_slot_event = true; + } + + queue_push_event(dev, EV_ABS, axis, + *slot_value(dev, slot, axis)); + } + } + } + + /* add one last slot event to make sure the client is on the same + slot as the kernel */ + + rc = ioctl(dev->fd, EVIOCGABS(ABS_MT_SLOT), &abs_info); + if (rc < 0) + goto out; + + dev->current_slot = abs_info.value; + + if (dev->current_slot != last_reported_slot) + queue_push_event(dev, EV_ABS, ABS_MT_SLOT, dev->current_slot); + + rc = 0; +out: + return rc ? -errno : 0; +} + +static int +read_more_events(struct libevdev *dev) +{ + int free_elem; + int len; + struct input_event *next; + + free_elem = queue_num_free_elements(dev); + if (free_elem <= 0) + return 0; + + next = queue_next_element(dev); + len = read(dev->fd, next, free_elem * sizeof(struct input_event)); + if (len < 0) + return -errno; + + if (len > 0 && len % sizeof(struct input_event) != 0) + return -EINVAL; + + if (len > 0) { + int nev = len/sizeof(struct input_event); + queue_set_num_elements(dev, queue_num_elements(dev) + nev); + } + + return 0; +} + +static inline void +drain_events(struct libevdev *dev) +{ + int rc; + size_t nelem; + int iterations = 0; + const int max_iterations = 8; /* EVDEV_BUF_PACKETS in + kernel/drivers/input/evedev.c */ + + queue_shift_multiple(dev, queue_num_elements(dev), NULL); + + do { + rc = read_more_events(dev); + if (rc == -EAGAIN) + return; + + if (rc < 0) { + log_error(dev, "Failed to drain events before sync.\n"); + return; + } + + nelem = queue_num_elements(dev); + queue_shift_multiple(dev, nelem, NULL); + } while (iterations++ < max_iterations && nelem >= queue_size(dev)); + + /* Our buffer should be roughly the same or bigger than the kernel + buffer in most cases, so we usually don't expect to recurse. If + we do, make sure we stop after max_iterations and proceed with + what we have. This could happen if events queue up faster than + we can drain them. + */ + if (iterations >= max_iterations) + log_info(dev, "Unable to drain events, buffer size mismatch.\n"); +} + +static int +sync_state(struct libevdev *dev) +{ + int rc = 0; + bool want_mt_sync = false; + int last_reported_slot = 0; + struct slot_change_state changes[dev->num_slots > 0 ? dev->num_slots : 1]; + + memset(changes, 0, sizeof(changes)); + + /* see section "Discarding events before synchronizing" in + * libevdev/libevdev.h */ + drain_events(dev); + + /* We generate one or two event frames during sync. + * The first one (if it exists) terminates all slots that have + * either terminated during SYN_DROPPED or changed their tracking + * ID. + * + * The second frame syncs everything up to the current state of the + * device - including re-starting those slots that have a changed + * tracking id. + */ + if (dev->num_slots > -1 && + libevdev_has_event_code(dev, EV_ABS, ABS_MT_SLOT)) { + want_mt_sync = true; + rc = sync_mt_state(dev, changes); + if (rc == 0) + terminate_slots(dev, changes, &last_reported_slot); + else + want_mt_sync = false; + } + + if (libevdev_has_event_type(dev, EV_KEY)) + rc = sync_key_state(dev); + if (libevdev_has_event_type(dev, EV_LED)) + rc = sync_led_state(dev); + if (libevdev_has_event_type(dev, EV_SW)) + rc = sync_sw_state(dev); + if (rc == 0 && libevdev_has_event_type(dev, EV_ABS)) + rc = sync_abs_state(dev); + if (rc == 0 && want_mt_sync) + push_mt_sync_events(dev, changes, last_reported_slot); + + dev->queue_nsync = queue_num_elements(dev); + + if (dev->queue_nsync > 0) { + queue_push_event(dev, EV_SYN, SYN_REPORT, 0); + dev->queue_nsync++; + } + + return rc; +} + +static int +update_key_state(struct libevdev *dev, const struct input_event *e) +{ + if (!libevdev_has_event_type(dev, EV_KEY)) + return 1; + + if (e->code > KEY_MAX) + return 1; + + set_bit_state(dev->key_values, e->code, e->value != 0); + + return 0; +} + +static int +update_mt_state(struct libevdev *dev, const struct input_event *e) +{ + if (e->code == ABS_MT_SLOT && dev->num_slots > -1) { + int i; + dev->current_slot = e->value; + /* sync abs_info with the current slot values */ + for (i = ABS_MT_SLOT + 1; i <= ABS_MT_MAX; i++) { + if (libevdev_has_event_code(dev, EV_ABS, i)) + dev->abs_info[i].value = *slot_value(dev, dev->current_slot, i); + } + + return 0; + } + + if (dev->current_slot == -1) + return 1; + + *slot_value(dev, dev->current_slot, e->code) = e->value; + + return 0; +} + +static int +update_abs_state(struct libevdev *dev, const struct input_event *e) +{ + if (!libevdev_has_event_type(dev, EV_ABS)) + return 1; + + if (e->code > ABS_MAX) + return 1; + + if (e->code >= ABS_MT_MIN && e->code <= ABS_MT_MAX) + update_mt_state(dev, e); + + dev->abs_info[e->code].value = e->value; + + return 0; +} + +static int +update_led_state(struct libevdev *dev, const struct input_event *e) +{ + if (!libevdev_has_event_type(dev, EV_LED)) + return 1; + + if (e->code > LED_MAX) + return 1; + + set_bit_state(dev->led_values, e->code, e->value != 0); + + return 0; +} + +static int +update_sw_state(struct libevdev *dev, const struct input_event *e) +{ + if (!libevdev_has_event_type(dev, EV_SW)) + return 1; + + if (e->code > SW_MAX) + return 1; + + set_bit_state(dev->sw_values, e->code, e->value != 0); + + return 0; +} + +static int +update_state(struct libevdev *dev, const struct input_event *e) +{ + int rc = 0; + + switch(e->type) { + case EV_SYN: + case EV_REL: + break; + case EV_KEY: + rc = update_key_state(dev, e); + break; + case EV_ABS: + rc = update_abs_state(dev, e); + break; + case EV_LED: + rc = update_led_state(dev, e); + break; + case EV_SW: + rc = update_sw_state(dev, e); + break; + } + + dev->last_event_time.tv_sec = e->input_event_sec; + dev->last_event_time.tv_usec = e->input_event_usec; + + return rc; +} + +/** + * Sanitize/modify events where needed. + */ +static inline enum event_filter_status +sanitize_event(const struct libevdev *dev, + struct input_event *ev, + enum SyncState sync_state) +{ + if (!libevdev_has_event_code(dev, ev->type, ev->code)) + return EVENT_FILTER_DISCARD; + + if (unlikely(dev->num_slots > -1 && + libevdev_event_is_code(ev, EV_ABS, ABS_MT_SLOT) && + (ev->value < 0 || ev->value >= dev->num_slots))) { + log_bug(dev, "Device \"%s\" received an invalid slot index %d." + "Capping to announced max slot number %d.\n", + dev->name, ev->value, dev->num_slots - 1); + ev->value = dev->num_slots - 1; + return EVENT_FILTER_MODIFIED; + + /* Drop any invalid tracking IDs, they are only supposed to go from + N to -1 or from -1 to N. Never from -1 to -1, or N to M. Very + unlikely to ever happen from a real device. + */ + } + + if (unlikely(sync_state == SYNC_NONE && + dev->num_slots > -1 && + libevdev_event_is_code(ev, EV_ABS, ABS_MT_TRACKING_ID) && + ((ev->value == -1 && + *slot_value(dev, dev->current_slot, ABS_MT_TRACKING_ID) == -1) || + (ev->value != -1 && + *slot_value(dev, dev->current_slot, ABS_MT_TRACKING_ID) != -1)))) { + log_bug(dev, "Device \"%s\" received a double tracking ID %d in slot %d.\n", + dev->name, ev->value, dev->current_slot); + return EVENT_FILTER_DISCARD; + } + + return EVENT_FILTER_NONE; +} + +LIBEVDEV_EXPORT int +libevdev_next_event(struct libevdev *dev, unsigned int flags, struct input_event *ev) +{ + int rc = LIBEVDEV_READ_STATUS_SUCCESS; + enum event_filter_status filter_status; + const unsigned int valid_flags = LIBEVDEV_READ_FLAG_NORMAL | + LIBEVDEV_READ_FLAG_SYNC | + LIBEVDEV_READ_FLAG_FORCE_SYNC | + LIBEVDEV_READ_FLAG_BLOCKING; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + if ((flags & valid_flags) == 0) { + log_bug(dev, "invalid flags %#x.\n", flags); + return -EINVAL; + } + + if (flags & LIBEVDEV_READ_FLAG_SYNC) { + if (dev->sync_state == SYNC_NEEDED) { + rc = sync_state(dev); + if (rc != 0) + return rc; + dev->sync_state = SYNC_IN_PROGRESS; + } + + if (dev->queue_nsync == 0) { + dev->sync_state = SYNC_NONE; + return -EAGAIN; + } + + } else if (dev->sync_state != SYNC_NONE) { + struct input_event e; + + /* call update_state for all events here, otherwise the library has the wrong view + of the device too */ + while (queue_shift(dev, &e) == 0) { + dev->queue_nsync--; + if (sanitize_event(dev, &e, dev->sync_state) != EVENT_FILTER_DISCARD) + update_state(dev, &e); + } + + dev->sync_state = SYNC_NONE; + } + + /* Always read in some more events. Best case this smoothes over a potential SYN_DROPPED, + worst case we don't read fast enough and end up with SYN_DROPPED anyway. + + Except if the fd is in blocking mode and we still have events from the last read, don't + read in any more. + */ + do { + if (queue_num_elements(dev) == 0) { + rc = read_more_events(dev); + if (rc < 0 && rc != -EAGAIN) + goto out; + } + + if (flags & LIBEVDEV_READ_FLAG_FORCE_SYNC) { + dev->sync_state = SYNC_NEEDED; + rc = LIBEVDEV_READ_STATUS_SYNC; + goto out; + } + + if (queue_shift(dev, ev) != 0) + return -EAGAIN; + + filter_status = sanitize_event(dev, ev, dev->sync_state); + if (filter_status != EVENT_FILTER_DISCARD) + update_state(dev, ev); + + /* if we disabled a code, get the next event instead */ + } while(filter_status == EVENT_FILTER_DISCARD || + !libevdev_has_event_code(dev, ev->type, ev->code)); + + rc = LIBEVDEV_READ_STATUS_SUCCESS; + if (ev->type == EV_SYN && ev->code == SYN_DROPPED) { + dev->sync_state = SYNC_NEEDED; + rc = LIBEVDEV_READ_STATUS_SYNC; + } + + if (flags & LIBEVDEV_READ_FLAG_SYNC && dev->queue_nsync > 0) { + dev->queue_nsync--; + rc = LIBEVDEV_READ_STATUS_SYNC; + if (dev->queue_nsync == 0) + dev->sync_state = SYNC_NONE; + } + +out: + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_has_event_pending(struct libevdev *dev) +{ + struct pollfd fds = { dev->fd, POLLIN, 0 }; + int rc; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + if (queue_num_elements(dev) != 0) + return 1; + + rc = poll(&fds, 1, 0); + return (rc >= 0) ? rc : -errno; +} + +LIBEVDEV_EXPORT const char * +libevdev_get_name(const struct libevdev *dev) +{ + return dev->name ? dev->name : ""; +} + +LIBEVDEV_EXPORT const char * +libevdev_get_phys(const struct libevdev *dev) +{ + return dev->phys; +} + +LIBEVDEV_EXPORT const char * +libevdev_get_uniq(const struct libevdev *dev) +{ + return dev->uniq; +} + +#define STRING_SETTER(field) \ +LIBEVDEV_EXPORT void libevdev_set_##field(struct libevdev *dev, const char *field) \ +{ \ + if (field == NULL) \ + return; \ + free(dev->field); \ + dev->field = strdup(field); \ +} + +STRING_SETTER(name) +STRING_SETTER(phys) +STRING_SETTER(uniq) + +#define PRODUCT_GETTER(name) \ +LIBEVDEV_EXPORT int libevdev_get_id_##name(const struct libevdev *dev) \ +{ \ + return dev->ids.name; \ +} + +PRODUCT_GETTER(product) +PRODUCT_GETTER(vendor) +PRODUCT_GETTER(bustype) +PRODUCT_GETTER(version) + +#define PRODUCT_SETTER(field) \ +LIBEVDEV_EXPORT void libevdev_set_id_##field(struct libevdev *dev, int field) \ +{ \ + dev->ids.field = field;\ +} + +PRODUCT_SETTER(product) +PRODUCT_SETTER(vendor) +PRODUCT_SETTER(bustype) +PRODUCT_SETTER(version) + +LIBEVDEV_EXPORT int +libevdev_get_driver_version(const struct libevdev *dev) +{ + return dev->driver_version; +} + +LIBEVDEV_EXPORT int +libevdev_has_property(const struct libevdev *dev, unsigned int prop) +{ + return (prop <= INPUT_PROP_MAX) && bit_is_set(dev->props, prop); +} + +LIBEVDEV_EXPORT int +libevdev_enable_property(struct libevdev *dev, unsigned int prop) +{ + if (prop > INPUT_PROP_MAX) + return -1; + + set_bit(dev->props, prop); + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_disable_property(struct libevdev *dev, unsigned int prop) +{ + if (prop > INPUT_PROP_MAX) + return -1; + + clear_bit(dev->props, prop); + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_has_event_type(const struct libevdev *dev, unsigned int type) +{ + return type == EV_SYN ||(type <= EV_MAX && bit_is_set(dev->bits, type)); +} + +LIBEVDEV_EXPORT int +libevdev_has_event_code(const struct libevdev *dev, unsigned int type, unsigned int code) +{ + const unsigned long *mask = NULL; + int max; + + if (!libevdev_has_event_type(dev, type)) + return 0; + + if (type == EV_SYN) + return 1; + + max = type_to_mask_const(dev, type, &mask); + + if (max == -1 || code > (unsigned int)max) + return 0; + + return bit_is_set(mask, code); +} + +LIBEVDEV_EXPORT int +libevdev_get_event_value(const struct libevdev *dev, unsigned int type, unsigned int code) +{ + int value = 0; + + if (!libevdev_has_event_type(dev, type) || !libevdev_has_event_code(dev, type, code)) + return 0; + + switch (type) { + case EV_ABS: value = dev->abs_info[code].value; break; + case EV_KEY: value = bit_is_set(dev->key_values, code); break; + case EV_LED: value = bit_is_set(dev->led_values, code); break; + case EV_SW: value = bit_is_set(dev->sw_values, code); break; + case EV_REP: + switch(code) { + case REP_DELAY: + libevdev_get_repeat(dev, &value, NULL); + break; + case REP_PERIOD: + libevdev_get_repeat(dev, NULL, &value); + break; + default: + value = 0; + break; + } + break; + default: + value = 0; + break; + } + + return value; +} + +LIBEVDEV_EXPORT int +libevdev_set_event_value(struct libevdev *dev, unsigned int type, unsigned int code, int value) +{ + int rc = 0; + struct input_event e; + + if (!libevdev_has_event_type(dev, type) || !libevdev_has_event_code(dev, type, code)) + return -1; + + e.type = type; + e.code = code; + e.value = value; + + if (sanitize_event(dev, &e, SYNC_NONE) != EVENT_FILTER_NONE) + return -1; + + switch(type) { + case EV_ABS: rc = update_abs_state(dev, &e); break; + case EV_KEY: rc = update_key_state(dev, &e); break; + case EV_LED: rc = update_led_state(dev, &e); break; + case EV_SW: rc = update_sw_state(dev, &e); break; + default: + rc = -1; + break; + } + + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_fetch_event_value(const struct libevdev *dev, unsigned int type, unsigned int code, int *value) +{ + if (libevdev_has_event_type(dev, type) && + libevdev_has_event_code(dev, type, code)) { + *value = libevdev_get_event_value(dev, type, code); + return 1; + } + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_get_slot_value(const struct libevdev *dev, unsigned int slot, unsigned int code) +{ + if (!libevdev_has_event_type(dev, EV_ABS) || !libevdev_has_event_code(dev, EV_ABS, code)) + return 0; + + if (dev->num_slots < 0 || slot >= (unsigned int)dev->num_slots) + return 0; + + if (code > ABS_MT_MAX || code < ABS_MT_MIN) + return 0; + + return *slot_value(dev, slot, code); +} + +LIBEVDEV_EXPORT int +libevdev_set_slot_value(struct libevdev *dev, unsigned int slot, unsigned int code, int value) +{ + if (!libevdev_has_event_type(dev, EV_ABS) || !libevdev_has_event_code(dev, EV_ABS, code)) + return -1; + + if (dev->num_slots == -1 || slot >= (unsigned int)dev->num_slots) + return -1; + + if (code > ABS_MT_MAX || code < ABS_MT_MIN) + return -1; + + if (code == ABS_MT_SLOT) { + if (value < 0 || value >= libevdev_get_num_slots(dev)) + return -1; + dev->current_slot = value; + } + + *slot_value(dev, slot, code) = value; + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_fetch_slot_value(const struct libevdev *dev, unsigned int slot, unsigned int code, int *value) +{ + if (libevdev_has_event_type(dev, EV_ABS) && + libevdev_has_event_code(dev, EV_ABS, code) && + dev->num_slots >= 0 && + slot < (unsigned int)dev->num_slots) { + *value = libevdev_get_slot_value(dev, slot, code); + return 1; + } + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_get_num_slots(const struct libevdev *dev) +{ + return dev->num_slots; +} + +LIBEVDEV_EXPORT int +libevdev_get_current_slot(const struct libevdev *dev) +{ + return dev->current_slot; +} + +LIBEVDEV_EXPORT const struct input_absinfo* +libevdev_get_abs_info(const struct libevdev *dev, unsigned int code) +{ + if (!libevdev_has_event_type(dev, EV_ABS) || + !libevdev_has_event_code(dev, EV_ABS, code)) + return NULL; + + return &dev->abs_info[code]; +} + +#define ABS_GETTER(name) \ +LIBEVDEV_EXPORT int libevdev_get_abs_##name(const struct libevdev *dev, unsigned int code) \ +{ \ + const struct input_absinfo *absinfo = libevdev_get_abs_info(dev, code); \ + return absinfo ? absinfo->name : 0; \ +} + +ABS_GETTER(maximum) +ABS_GETTER(minimum) +ABS_GETTER(fuzz) +ABS_GETTER(flat) +ABS_GETTER(resolution) + +#define ABS_SETTER(field) \ +LIBEVDEV_EXPORT void libevdev_set_abs_##field(struct libevdev *dev, unsigned int code, int val) \ +{ \ + if (!libevdev_has_event_code(dev, EV_ABS, code)) \ + return; \ + dev->abs_info[code].field = val; \ +} + +ABS_SETTER(maximum) +ABS_SETTER(minimum) +ABS_SETTER(fuzz) +ABS_SETTER(flat) +ABS_SETTER(resolution) + +LIBEVDEV_EXPORT void +libevdev_set_abs_info(struct libevdev *dev, unsigned int code, const struct input_absinfo *abs) +{ + if (!libevdev_has_event_code(dev, EV_ABS, code)) + return; + + dev->abs_info[code] = *abs; +} + +LIBEVDEV_EXPORT int +libevdev_enable_event_type(struct libevdev *dev, unsigned int type) +{ + int max; + + if (type > EV_MAX) + return -1; + + if (libevdev_has_event_type(dev, type)) + return 0; + + max = libevdev_event_type_get_max(type); + if (max == -1) + return -1; + + set_bit(dev->bits, type); + + if (type == EV_REP) { + int delay = 0, period = 0; + libevdev_enable_event_code(dev, EV_REP, REP_DELAY, &delay); + libevdev_enable_event_code(dev, EV_REP, REP_PERIOD, &period); + } + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_disable_event_type(struct libevdev *dev, unsigned int type) +{ + int max; + + if (type > EV_MAX || type == EV_SYN) + return -1; + + max = libevdev_event_type_get_max(type); + if (max == -1) + return -1; + + clear_bit(dev->bits, type); + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_enable_event_code(struct libevdev *dev, unsigned int type, + unsigned int code, const void *data) +{ + unsigned int max; + unsigned long *mask = NULL; + + if (libevdev_enable_event_type(dev, type)) + return -1; + + switch(type) { + case EV_SYN: + return 0; + case EV_ABS: + case EV_REP: + if (data == NULL) + return -1; + break; + default: + if (data != NULL) + return -1; + break; + } + + max = type_to_mask(dev, type, &mask); + + if (code > max || (int)max == -1) + return -1; + + set_bit(mask, code); + + if (type == EV_ABS) { + const struct input_absinfo *abs = data; + dev->abs_info[code] = *abs; + if (code == ABS_MT_SLOT) { + if (init_slots(dev) != 0) + return -1; + } else if (code == ABS_MT_TRACKING_ID) { + reset_tracking_ids(dev); + } + } else if (type == EV_REP) { + const int *value = data; + dev->rep_values[code] = *value; + } + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_disable_event_code(struct libevdev *dev, unsigned int type, unsigned int code) +{ + unsigned int max; + unsigned long *mask = NULL; + + if (type > EV_MAX || type == EV_SYN) + return -1; + + max = type_to_mask(dev, type, &mask); + + if (code > max || (int)max == -1) + return -1; + + clear_bit(mask, code); + + if (type == EV_ABS) { + if (code == ABS_MT_SLOT) { + if (init_slots(dev) != 0) + return -1; + } else if (code == ABS_MT_TRACKING_ID) { + reset_tracking_ids(dev); + } + } + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_kernel_set_abs_info(struct libevdev *dev, unsigned int code, const struct input_absinfo *abs) +{ + int rc; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + if (code > ABS_MAX) + return -EINVAL; + + rc = ioctl(dev->fd, EVIOCSABS(code), abs); + if (rc < 0) + rc = -errno; + else + rc = libevdev_enable_event_code(dev, EV_ABS, code, abs); + + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_grab(struct libevdev *dev, enum libevdev_grab_mode grab) +{ + int rc = 0; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + if (grab != LIBEVDEV_GRAB && grab != LIBEVDEV_UNGRAB) { + log_bug(dev, "invalid grab parameter %#x\n", grab); + return -EINVAL; + } + + if (grab == dev->grabbed) + return 0; + + if (grab == LIBEVDEV_GRAB) + rc = ioctl(dev->fd, EVIOCGRAB, (void *)1); + else if (grab == LIBEVDEV_UNGRAB) + rc = ioctl(dev->fd, EVIOCGRAB, (void *)0); + + if (rc == 0) + dev->grabbed = grab; + + return rc < 0 ? -errno : 0; +} + +LIBEVDEV_EXPORT int +libevdev_event_is_type(const struct input_event *ev, unsigned int type) +{ + return type < EV_CNT && ev->type == type; +} + +LIBEVDEV_EXPORT int +libevdev_event_is_code(const struct input_event *ev, unsigned int type, unsigned int code) +{ + int max; + + if (!libevdev_event_is_type(ev, type)) + return 0; + + max = libevdev_event_type_get_max(type); + return (max > -1 && code <= (unsigned int)max && ev->code == code); +} + +LIBEVDEV_EXPORT const char* +libevdev_event_type_get_name(unsigned int type) +{ + if (type > EV_MAX) + return NULL; + + return ev_map[type]; +} + +LIBEVDEV_EXPORT const char* +libevdev_event_code_get_name(unsigned int type, unsigned int code) +{ + int max = libevdev_event_type_get_max(type); + + if (max == -1 || code > (unsigned int)max) + return NULL; + + return event_type_map[type][code]; +} + +LIBEVDEV_EXPORT const char * +libevdev_event_value_get_name(unsigned int type, + unsigned int code, + int value) +{ + /* This is a simplified version because nothing else + is an enum like ABS_MT_TOOL_TYPE so we don't need + a generic lookup */ + if (type != EV_ABS || code != ABS_MT_TOOL_TYPE) + return NULL; + + if (value < 0 || value > MT_TOOL_MAX) + return NULL; + + return mt_tool_map[value]; +} + +LIBEVDEV_EXPORT const char* +libevdev_property_get_name(unsigned int prop) +{ + if (prop > INPUT_PROP_MAX) + return NULL; + + return input_prop_map[prop]; +} + +LIBEVDEV_EXPORT int +libevdev_event_type_get_max(unsigned int type) +{ + if (type > EV_MAX) + return -1; + + return ev_max[type]; +} + +LIBEVDEV_EXPORT int +libevdev_get_repeat(const struct libevdev *dev, int *delay, int *period) +{ + if (!libevdev_has_event_type(dev, EV_REP)) + return -1; + + if (delay != NULL) + *delay = dev->rep_values[REP_DELAY]; + if (period != NULL) + *period = dev->rep_values[REP_PERIOD]; + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_kernel_set_led_value(struct libevdev *dev, unsigned int code, enum libevdev_led_value value) +{ + return libevdev_kernel_set_led_values(dev, code, value, -1); +} + +LIBEVDEV_EXPORT int +libevdev_kernel_set_led_values(struct libevdev *dev, ...) +{ + struct input_event ev[LED_MAX + 1]; + enum libevdev_led_value val; + va_list args; + int code; + int rc = 0; + size_t nleds = 0; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + memset(ev, 0, sizeof(ev)); + + va_start(args, dev); + code = va_arg(args, unsigned int); + while (code != -1) { + if (code > LED_MAX) { + rc = -EINVAL; + break; + } + val = va_arg(args, enum libevdev_led_value); + if (val != LIBEVDEV_LED_ON && val != LIBEVDEV_LED_OFF) { + rc = -EINVAL; + break; + } + + if (libevdev_has_event_code(dev, EV_LED, code)) { + struct input_event *e = ev; + + while (e->type > 0 && e->code != code) + e++; + + if (e->type == 0) + nleds++; + e->type = EV_LED; + e->code = code; + e->value = (val == LIBEVDEV_LED_ON); + } + code = va_arg(args, unsigned int); + } + va_end(args); + + if (rc == 0 && nleds > 0) { + ev[nleds].type = EV_SYN; + ev[nleds++].code = SYN_REPORT; + + rc = write(libevdev_get_fd(dev), ev, nleds * sizeof(ev[0])); + if (rc > 0) { + nleds--; /* last is EV_SYN */ + while (nleds--) + update_led_state(dev, &ev[nleds]); + } + rc = (rc != -1) ? 0 : -errno; + } + + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_set_clock_id(struct libevdev *dev, int clockid) +{ + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + return ioctl(dev->fd, EVIOCSCLOCKID, &clockid) ? -errno : 0; +} diff --git a/nativelib/src/main/cpp/libevdev/libevdev.h b/nativelib/src/main/cpp/libevdev/libevdev.h new file mode 100644 index 0000000000..142dac2198 --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev.h @@ -0,0 +1,2379 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +#ifndef LIBEVDEV_H +#define LIBEVDEV_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define LIBEVDEV_ATTRIBUTE_PRINTF(_format, _args) __attribute__ ((format (printf, _format, _args))) + +/** + * @mainpage + * + * **libevdev** is a library for handling evdev kernel devices. It abstracts + * the \ref ioctls through type-safe interfaces and provides functions to change + * the appearance of the device. + * + * Development + * =========== + * The git repository is available here: + * + * - https://gitlab.freedesktop.org/libevdev/libevdev + * + * Development of libevdev is discussed on + * [input-tools@lists.freedesktop.org](http://lists.freedesktop.org/mailman/listinfo/input-tools). + * Please submit patches, questions or general comments there. + * + * Handling events and SYN_DROPPED + * =============================== + * + * libevdev provides an interface for handling events, including most notably + * `SYN_DROPPED` events. `SYN_DROPPED` events are sent by the kernel when the + * process does not read events fast enough and the kernel is forced to drop + * some events. This causes the device to get out of sync with the process' + * view of it. libevdev handles this by telling the caller that a * `SYN_DROPPED` + * has been received and that the state of the device is different to what is + * to be expected. It then provides the delta between the previous state and + * the actual state of the device as a set of events. See + * libevdev_next_event() and @ref syn_dropped for more information on how + * `SYN_DROPPED` is handled. + * + * Signal safety + * ============= + * + * libevdev is signal-safe for the majority of its operations, i.e. many of + * its functions are safe to be called from within a signal handler. + * Check the API documentation to make sure, unless explicitly stated a call + * is not signal safe. + * + * Device handling + * =============== + * + * A libevdev context is valid for a given file descriptor and its + * duration. Closing the file descriptor will not destroy the libevdev device + * but libevdev will not be able to read further events. + * + * libevdev does not attempt duplicate detection. Initializing two libevdev + * devices for the same fd is valid and behaves the same as for two different + * devices. + * + * libevdev does not handle the file descriptors directly, it merely uses + * them. The caller is responsible for opening the file descriptors, setting + * them to `O_NONBLOCK` and handling permissions. A caller should drain any + * events pending on the file descriptor before passing it to libevdev. + * + * Where does libevdev sit? + * ======================== + * + * libevdev is essentially a `read(2)` on steroids for `/dev/input/eventX` + * devices. It sits below the process that handles input events, in between + * the kernel and that process. In the simplest case, e.g. an evtest-like tool + * the stack would look like this: + * + * kernel → libevdev → evtest + * + * For X.Org input modules, the stack would look like this: + * + * kernel → libevdev → xf86-input-evdev → X server → X client + * + * For anything using libinput (e.g. most Wayland compositors), the stack + * the stack would look like this: + * + * kernel → libevdev → libinput → Compositor → Wayland client + * + * libevdev does **not** have knowledge of X clients or Wayland clients, it is + * too low in the stack. + * + * Example + * ======= + * Below is a simple example that shows how libevdev could be used. This example + * opens a device, checks for relative axes and a left mouse button and if it + * finds them monitors the device to print the event. + * + * @code + * struct libevdev *dev = NULL; + * int fd; + * int rc = 1; + * + * fd = open("/dev/input/event0", O_RDONLY|O_NONBLOCK); + * rc = libevdev_new_from_fd(fd, &dev); + * if (rc < 0) { + * fprintf(stderr, "Failed to init libevdev (%s)\n", strerror(-rc)); + * exit(1); + * } + * printf("Input device name: \"%s\"\n", libevdev_get_name(dev)); + * printf("Input device ID: bus %#x vendor %#x product %#x\n", + * libevdev_get_id_bustype(dev), + * libevdev_get_id_vendor(dev), + * libevdev_get_id_product(dev)); + * if (!libevdev_has_event_type(dev, EV_REL) || + * !libevdev_has_event_code(dev, EV_KEY, BTN_LEFT)) { + * printf("This device does not look like a mouse\n"); + * exit(1); + * } + * + * do { + * struct input_event ev; + * rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); + * if (rc == 0) + * printf("Event: %s %s %d\n", + * libevdev_event_type_get_name(ev.type), + * libevdev_event_code_get_name(ev.type, ev.code), + * ev.value); + * } while (rc == 1 || rc == 0 || rc == -EAGAIN); + * @endcode + * + * A more complete example is available with the libevdev-events tool here: + * https://gitlab.freedesktop.org/libevdev/libevdev/blob/master/tools/libevdev-events.c + * + * Backwards compatibility with older kernel + * ========================================= + * libevdev attempts to build and run correctly on a number of kernel versions. + * If features required are not available, libevdev attempts to work around them + * in the most reasonable way. For more details see \ref backwardscompatibility. + * + * License information + * =================== + * libevdev is licensed under the + * [MIT license](http://cgit.freedesktop.org/libevdev/tree/COPYING). + * + * Bindings + * =================== + * - Python: https://gitlab.freedesktop.org/libevdev/python-libevdev + * - Haskell: http://hackage.haskell.org/package/evdev + * - Rust: https://crates.io/crates/evdev-rs + * + * Reporting bugs + * ============== + * Please report bugs in the freedesktop.org GitLab instance: + * https://gitlab.freedesktop.org/libevdev/libevdev/issues/ + */ + +/** + * @page syn_dropped SYN_DROPPED handling + * + * This page describes how libevdev handles `SYN_DROPPED` events. + * + * Receiving `SYN_DROPPED` events + * ============================== + * + * The kernel sends evdev events separated by an event of type `EV_SYN` and + * code `SYN_REPORT`. Such an event marks the end of a frame of hardware + * events. The number of events between `SYN_REPORT` events is arbitrary and + * depends on the hardware. An example event sequence may look like this: + * @code + * EV_ABS ABS_X 9 + * EV_ABS ABS_Y 8 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_X 10 + * EV_ABS ABS_Y 10 + * EV_KEY BTN_TOUCH 1 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_X 11 + * EV_SYN SYN_REPORT 0 + * @endcode + * + * Events are handed to the client buffer as they appear, the kernel adjusts + * the buffer size to handle at least one full event. In the normal case, + * the client reads the event and the kernel can place the next event in the + * buffer. If the client is not fast enough, the kernel places an event of + * type `EV_SYN` and code `SYN_DROPPED` into the buffer, effectively notifying + * the client that some events were lost. The above example event sequence + * may look like this (note the missing/repeated events): + * @code + * EV_ABS ABS_X 9 + * EV_ABS ABS_Y 8 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_X 10 + * EV_ABS ABS_Y 10 + * EV_SYN SYN_DROPPED 0 + * EV_ABS ABS_Y 15 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_X 11 + * EV_KEY BTN_TOUCH 0 + * EV_SYN SYN_REPORT 0 + * @endcode + * + * A `SYN_DROPPED` event may be recieved at any time in the event sequence. + * When a `SYN_DROPPED` event is received, the client must: + * * discard all events since the last `SYN_REPORT` + * * discard all events until including the next `SYN_REPORT` + * These event are part of incomplete event frames. + * + * Synchronizing the state of the device + * ===================================== + * + * The handling of the device after a `SYN_DROPPED` depends on the available + * event codes. For all event codes of type `EV_REL`, no handling is + * necessary, there is no state attached. For all event codes of type + * `EV_KEY`, `EV_SW`, `EV_LED` and `EV_SND`, the matching @ref ioctls retrieve the + * current state. The caller must then compare the last-known state to the + * retrieved state and handle the deltas accordingly. + * libevdev simplifies this approach: if the state of the device has + * changed, libevdev generates an event for each code with the new value and + * passes it to the caller during libevdev_next_event() if + * @ref LIBEVDEV_READ_FLAG_SYNC is set. + * + * For events of type `EV_ABS` and an event code less than `ABS_MT_SLOT`, the + * handling of state changes is as described above. For events between + * `ABS_MT_SLOT` and `ABS_MAX`, the event handling differs. + * Slots are the vehicles to transport information for multiple simultaneous + * touchpoints on a device. Slots are re-used once a touchpoint has ended. + * The kernel sends an `ABS_MT_SLOT` event whenever the current slot + * changes; any event in the above axis range applies only to the currently + * active slot. + * Thus, an event sequence from a slot-capable device may look like this: + * @code + * EV_ABS ABS_MT_POSITION_Y 10 + * EV_ABS ABS_MT_SLOT 1 + * EV_ABS ABS_MT_POSITION_X 100 + * EV_ABS ABS_MT_POSITION_Y 80 + * EV_SYN SYN_REPORT 0 + * @endcode + * Note the lack of `ABS_MT_SLOT`: the first `ABS_MT_POSITION_Y` applies to + * a slot opened previously, and is the only axis that changed for that + * slot. The touchpoint in slot 1 now has position `100/80`. + * The kernel does not provide events if a value does not change, and does + * not send `ABS_MT_SLOT` events if the slot does not change, or none of the + * values within a slot changes. A client must thus keep the state for each + * slot. + * + * If a `SYN_DROPPED` is received, the client must sync all slots + * individually and update its internal state. libevdev simplifies this by + * generating multiple events: + * * for each slot on the device, libevdev generates an + * `ABS_MT_SLOT` event with the value set to the slot number + * * for each event code between `ABS_MT_SLOT + 1` and `ABS_MAX` that changed + * state for this slot, libevdev generates an event for the new state + * * libevdev sends a final `ABS_MT_SLOT` event for the current slot as + * seen by the kernel + * * libevdev terminates this sequence with an `EV_SYN SYN_REPORT` event + * + * An example event sequence for such a sync may look like this: + * @code + * EV_ABS ABS_MT_SLOT 0 + * EV_ABS ABS_MT_POSITION_Y 10 + * EV_ABS ABS_MT_SLOT 1 + * EV_ABS ABS_MT_POSITION_X 100 + * EV_ABS ABS_MT_POSITION_Y 80 + * EV_ABS ABS_MT_SLOT 2 + * EV_ABS ABS_MT_POSITION_Y 8 + * EV_ABS ABS_MT_PRESSURE 12 + * EV_ABS ABS_MT_SLOT 1 + * EV_SYN SYN_REPORT 0 + * @endcode + * Note the terminating `ABS_MT_SLOT` event, this indicates that the kernel + * currently has slot 1 active. + * + * Synchronizing ABS_MT_TRACKING_ID + * ================================ + * + * The event code `ABS_MT_TRACKING_ID` is used to denote the start and end of + * a touch point within a slot. An `ABS_MT_TRACKING_ID` of zero or greater + * denotes the start of a touchpoint, an `ABS_MT_TRACKING_ID` of -1 denotes + * the end of a touchpoint within this slot. During `SYN_DROPPED`, a touch + * point may have ended and re-started within a slot - a client must check + * the `ABS_MT_TRACKING_ID`. libevdev simplifies this by emulating extra + * events if the `ABS_MT_TRACKING_ID` has changed: + * * if the `ABS_MT_TRACKING_ID` was valid and is -1, libevdev enqueues an + * `ABS_MT_TRACKING_ID` event with value -1. + * * if the `ABS_MT_TRACKING_ID` was -1 and is now a valid ID, libevdev + * enqueues an `ABS_MT_TRACKING_ID` event with the current value. + * * if the `ABS_MT_TRACKING_ID` was a valid ID and is now a different valid + * ID, libevev enqueues an `ABS_MT_TRACKING_ID` event with value -1 and + * another `ABS_MT_TRACKING_ID` event with the new value. + * + * An example event sequence for such a sync may look like this: + * @code + * EV_ABS ABS_MT_SLOT 0 + * EV_ABS ABS_MT_TRACKING_ID -1 + * EV_ABS ABS_MT_SLOT 2 + * EV_ABS ABS_MT_TRACKING_ID -1 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_MT_SLOT 1 + * EV_ABS ABS_MT_POSITION_X 100 + * EV_ABS ABS_MT_POSITION_Y 80 + * EV_ABS ABS_MT_SLOT 2 + * EV_ABS ABS_MT_TRACKING_ID 45 + * EV_ABS ABS_MT_POSITION_Y 8 + * EV_ABS ABS_MT_PRESSURE 12 + * EV_ABS ABS_MT_SLOT 1 + * EV_SYN SYN_REPORT 0 + * @endcode + * Note how the touchpoint in slot 0 was terminated, the touchpoint in slot + * 2 was terminated and then started with a new `ABS_MT_TRACKING_ID`. The touchpoint + * in slot 1 maintained the same `ABS_MT_TRACKING_ID` and only updated the + * coordinates. Slot 1 is the currently active slot. + * + * In the case of a `SYN_DROPPED` event, a touch point may be invisible to a + * client if it started after `SYN_DROPPED` and finished before the client + * handles events again. The below example shows an example event sequence + * and what libevdev sees in the case of a `SYN_DROPPED` event: + * @code + * + * kernel | userspace + * | + * EV_ABS ABS_MT_SLOT 0 | EV_ABS ABS_MT_SLOT 0 + * EV_ABS ABS_MT_TRACKING_ID -1 | EV_ABS ABS_MT_TRACKING_ID -1 + * EV_SYN SYN_REPORT 0 | EV_SYN SYN_REPORT 0 + * ------------------------ | ------------------------ + * EV_ABS ABS_MT_TRACKING_ID 30 | + * EV_ABS ABS_MT_POSITION_X 100 | + * EV_ABS ABS_MT_POSITION_Y 80 | + * EV_SYN SYN_REPORT 0 | SYN_DROPPED + * ------------------------ | + * EV_ABS ABS_MT_TRACKING_ID -1 | + * EV_SYN SYN_REPORT 0 | + * ------------------------ | ------------------------ + * EV_ABS ABS_MT_SLOT 1 | EV_ABS ABS_MT_SLOT 1 + * EV_ABS ABS_MT_POSITION_X 90 | EV_ABS ABS_MT_POSITION_X 90 + * EV_ABS ABS_MT_POSITION_Y 10 | EV_ABS ABS_MT_POSITION_Y 10 + * EV_SYN SYN_REPORT 0 | EV_SYN SYN_REPORT 0 + * @endcode + * If such an event sequence occurs, libevdev will send all updated axes + * during the sync process. Axis events may thus be generated for devices + * without a currently valid `ABS_MT_TRACKING_ID`. Specifically for the above + * example, the client would receive the following event sequence: + * @code + * EV_ABS ABS_MT_SLOT 0 ← LIBEVDEV_READ_FLAG_NORMAL + * EV_ABS ABS_MT_TRACKING_ID -1 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_SYN SYN_DROPPED 0 → LIBEVDEV_READ_STATUS_SYNC + * ------------------------ + * EV_ABS ABS_MT_POSITION_X 100 ← LIBEVDEV_READ_FLAG_SYNC + * EV_ABS ABS_MT_POSITION_Y 80 + * EV_SYN SYN_REPORT 0 + * ----------------------------- → -EGAIN + * EV_ABS ABS_MT_SLOT 1 ← LIBEVDEV_READ_FLAG_NORMAL + * EV_ABS ABS_MT_POSITION_X 90 + * EV_ABS ABS_MT_POSITION_Y 10 + * EV_SYN SYN_REPORT 0 + * ------------------- + * @endcode + * The axis events do not reflect the position of a current touch point, a + * client must take care not to generate a new touch point based on those + * updates. + * + * Discarding events before synchronizing + * ===================================== + * + * The kernel implements the client buffer as a ring buffer. `SYN_DROPPED` + * events are handled when the buffer is full and a new event is received + * from a device. All existing events are discarded, a `SYN_DROPPED` is added + * to the buffer followed by the actual device event. Further events will be + * appended to the buffer until it is either read by the client, or filled + * again, at which point the sequence repeats. + * + * When the client reads the buffer, the buffer will thus always consist of + * exactly one `SYN_DROPPED` event followed by an unspecified number of real + * events. The data the ioctls return is the current state of the device, + * i.e. the state after all these events have been processed. For example, + * assume the buffer contains the following sequence: + * + * @code + * EV_SYN SYN_DROPPED + * EV_ABS ABS_X 1 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 2 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 3 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 4 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 5 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 6 + * EV_SYN SYN_REPORT 0 + * @endcode + * An ioctl at any time in this sequence will return a value of 6 for ABS_X. + * + * libevdev discards all events after a `SYN_DROPPED` to ensure the events + * during @ref LIBEVDEV_READ_FLAG_SYNC represent the last known state of the + * device. This loses some granularity of the events especially as the time + * between the `SYN_DROPPED` and the sync process increases. It does however + * avoid spurious cursor movements. In the above example, the event sequence + * by libevdev is: + * @code + * EV_SYN SYN_DROPPED + * EV_ABS ABS_X 6 + * EV_SYN SYN_REPORT 0 + * @endcode + */ + +/** + * @page backwardscompatibility Compatibility and Behavior across kernel versions + * + * This page describes libevdev's behavior when the build-time kernel and the + * run-time kernel differ in their feature set. + * + * With the exception of event names, libevdev defines features that may be + * missing on older kernels and building on such kernels will not disable + * features. Running libevdev on a kernel that is missing some feature will + * change libevdev's behavior. In most cases, the new behavior should be + * obvious, but it is spelled out below in detail. + * + * Minimum requirements + * ==================== + * libevdev requires a 2.6.36 kernel as minimum. Specifically, it requires + * kernel-support for `ABS_MT_SLOT`. + * + * Event and input property names + * ============================== + * Event names and input property names are defined at build-time by the + * linux/input.h shipped with libevdev. + * The list of event names is compiled at build-time, any events not defined + * at build time will not resolve. Specifically, + * libevdev_event_code_get_name() for an undefined type or code will + * always return `NULL`. Likewise, libevdev_property_get_name() will return NULL + * for properties undefined at build-time. + * + * Input properties + * ================ + * If the kernel does not support input properties, specifically the + * `EVIOCGPROPS` ioctl, libevdev does not expose input properties to the caller. + * Specifically, libevdev_has_property() will always return 0 unless the + * property has been manually set with libevdev_enable_property(). + * + * This also applies to the libevdev-uinput code. If uinput does not honor + * `UI_SET_PROPBIT`, libevdev will continue without setting the properties on + * the device. + * + * MT slot behavior + * ================= + * If the kernel does not support the `EVIOCGMTSLOTS` ioctl, libevdev + * assumes all values in all slots are 0 and continues without an error. + * + * SYN_DROPPED behavior + * ==================== + * A kernel without `SYN_DROPPED` won't send such an event. libevdev_next_event() + * will never require the switch to sync mode. + */ + +/** + * @page ioctls evdev ioctls + * + * This page lists the status of the evdev-specific ioctls in libevdev. + * + *
+ *
EVIOCGVERSION:
+ *
supported, see libevdev_get_driver_version()
+ *
EVIOCGID:
+ *
supported, see libevdev_get_id_product(), libevdev_get_id_vendor(), + * libevdev_get_id_bustype(), libevdev_get_id_version()
+ *
EVIOCGREP:
+ *
supported, see libevdev_get_event_value())
+ *
EVIOCSREP:
+ *
supported, see libevdev_enable_event_code()
+ *
EVIOCGKEYCODE:
+ *
currently not supported
+ *
EVIOCSKEYCODE:
+ *
currently not supported
+ *
EVIOCGKEYCODE_V2:
+ *
currently not supported
+ *
EVIOCSKEYCODE_V2:
+ *
currently not supported
+ *
EVIOCGNAME:
+ *
supported, see libevdev_get_name()
+ *
EVIOCGPHYS:
+ *
supported, see libevdev_get_phys()
+ *
EVIOCGUNIQ:
+ *
supported, see libevdev_get_uniq()
+ *
EVIOCGPROP:
+ *
supported, see libevdev_has_property()
+ *
EVIOCGMTSLOTS:
+ *
supported, see libevdev_get_num_slots(), libevdev_get_slot_value()
+ *
EVIOCGKEY:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value()
+ *
EVIOCGLED:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value()
+ *
EVIOCGSND:
+ *
currently not supported
+ *
EVIOCGSW:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value()
+ *
EVIOCGBIT:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value()
+ *
EVIOCGABS:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value(), + * libevdev_get_abs_info()
+ *
EVIOCSABS:
+ *
supported, see libevdev_kernel_set_abs_info()
+ *
EVIOCSFF:
+ *
currently not supported
+ *
EVIOCRMFF:
+ *
currently not supported
+ *
EVIOCGEFFECTS:
+ *
currently not supported
+ *
EVIOCGRAB:
+ *
supported, see libevdev_grab()
+ *
EVIOCSCLOCKID:
+ *
supported, see libevdev_set_clock_id()
+ *
EVIOCREVOKE:
+ *
currently not supported, see + * http://lists.freedesktop.org/archives/input-tools/2014-January/000688.html
+ *
EVIOCGMASK:
+ *
currently not supported
+ *
EVIOCSMASK:
+ *
currently not supported
+ *
+ * + */ + +/** + * @page kernel_header Kernel header + * + * libevdev provides its own copy of the Linux kernel header file and + * compiles against the definitions define here. Event type and event code + * names, etc. are taken from the file below: + * @include linux/input.h + */ + +/** + * @page static_linking Statically linking libevdev + * + * Statically linking libevdev.a is not recommended. Symbol visibility is + * difficult to control in a static library, so extra care must be taken to + * only use symbols that are explicitly exported. libevdev's API stability + * guarantee only applies to those symbols. + * + * If you do link libevdev statically, note that in addition to the exported + * symbols, libevdev reserves the _libevdev_* namespace. Do not use + * or create symbols with that prefix, they are subject to change at any + * time. + */ + +/** + * @page testing libevdev-internal test suite + * + * libevdev's internal test suite uses the + * [Check unit testing framework](http://check.sourceforge.net/). Tests are + * divided into test suites and test cases. Most tests create a uinput device, + * so you'll need to run as root, and your kernel must have + * `CONFIG_INPUT_UINPUT` enabled. + * + * To run a specific suite only: + * + * export CK_RUN_SUITE="suite name" + * + * To run a specific test case only: + * + * export CK_RUN_TEST="test case name" + * + * To get a list of all suites or tests: + * + * git grep "suite_create" + * git grep "tcase_create" + * + * By default, Check forks, making debugging harder. The test suite tries to detect + * if it is running inside gdb and disable forking. If that doesn't work for + * some reason, run gdb as below to avoid forking. + * + * sudo CK_FORK=no CK_RUN_TEST="test case name" gdb ./test/test-libevdev + * + * A special target `make gcov-report.txt` exists that runs gcov and leaves a + * `libevdev.c.gcov` file. Check that for test coverage. + * + * `make check` is hooked up to run the test and gcov (again, needs root). + * + * The test suite creates a lot of devices, very quickly. Add the following + * xorg.conf.d snippet to avoid the devices being added as X devices (at the + * time of writing, mutter can't handle these devices and exits after getting + * a BadDevice error). + * + * $ cat /etc/X11/xorg.conf.d/99-ignore-libevdev-devices.conf + * Section "InputClass" + * Identifier "Ignore libevdev test devices" + * MatchProduct "libevdev test device" + * Option "Ignore" "on" + * EndSection + * + */ + +/** + * @defgroup init Initialization and setup + * + * Initialization, initial setup and file descriptor handling. + * These functions are the main entry points for users of libevdev, usually a + * caller will use this series of calls: + * + * @code + * struct libevdev *dev; + * int err; + * + * dev = libevdev_new(); + * if (!dev) + * return ENOMEM; + * + * err = libevdev_set_fd(dev, fd); + * if (err < 0) + * printf("Failed (errno %d): %s\n", -err, strerror(-err)); + * + * libevdev_free(dev); + * @endcode + * + * libevdev_set_fd() is the central call and initializes the internal structs + * for the device at the given fd. libevdev functions will fail if called + * before libevdev_set_fd() unless documented otherwise. + */ + +/** + * @defgroup logging Library logging facilities + * + * libevdev provides two methods of logging library-internal messages. The + * old method is to provide a global log handler in + * libevdev_set_log_function(). The new method is to provide a per-context + * log handler in libevdev_set_device_log_function(). Developers are encouraged + * to use the per-context logging facilities over the global log handler as + * it provides access to the libevdev instance that caused a message, and is + * more flexible when libevdev is used from within a shared library. + * + * If a caller sets both the global log handler and a per-context log + * handler, each device with a per-context log handler will only invoke that + * log handler. + * + * @note To set a context-specific log handler, a context is needed. + * Thus developers are discouraged from using libevdev_new_from_fd() as + * important messages from the device initialization process may get lost. + * + * @note A context-specific handler cannot be used for libevdev's uinput + * devices. @ref uinput must use the global log handler. + */ + +/** + * @defgroup bits Querying device capabilities + * + * Abstraction functions to handle device capabilities, specifically + * device properties such as the name of the device and the bits + * representing the events supported by this device. + * + * The logical state returned may lag behind the physical state of the device. + * libevdev queries the device state on libevdev_set_fd() and then relies on + * the caller to parse events through libevdev_next_event(). If a caller does not + * use libevdev_next_event(), libevdev will not update the internal state of the + * device and thus returns outdated values. + */ + +/** + * @defgroup mt Multi-touch related functions + * Functions for querying multi-touch-related capabilities. MT devices + * following the kernel protocol B (using `ABS_MT_SLOT`) provide multiple touch + * points through so-called slots on the same axis. The slots are enumerated, + * a client reading from the device will first get an ABS_MT_SLOT event, then + * the values of axes changed in this slot. Multiple slots may be provided in + * before an `EV_SYN` event. + * + * As with @ref bits, the logical state of the device as seen by the library + * depends on the caller using libevdev_next_event(). + * + * The Linux kernel requires all axes on a device to have a semantic + * meaning, matching the axis names in linux/input.h. Some devices merely + * export a number of axes beyond the available axis list. For those + * devices, the multitouch information is invalid. Specifically, if a device + * provides the `ABS_MT_SLOT` axis AND also the `ABS_RESERVED` axis, the + * device is not treated as multitouch device. No slot information is + * available and the `ABS_MT` axis range for these devices is treated as all + * other `EV_ABS` axes. + * + * Note that because of limitations in the kernel API, such fake multitouch + * devices can not be reliably synced after a `SYN_DROPPED` event. libevdev + * ignores all `ABS_MT` axis values during the sync process and instead + * relies on the device to send the current axis value with the first event + * after `SYN_DROPPED`. + */ + +/** + * @defgroup kernel Modifying the appearance or capabilities of the device + * + * Modifying the set of events reported by this device. By default, the + * libevdev device mirrors the kernel device, enabling only those bits + * exported by the kernel. This set of functions enable or disable bits as + * seen from the caller. + * + * Enabling an event type or code does not affect event reporting - a + * software-enabled event will not be generated by the physical hardware. + * Disabling an event will prevent libevdev from routing such events to the + * caller. Enabling and disabling event types and codes is at the library + * level and thus only affects the caller. + * + * If an event type or code is enabled at kernel-level, future users of this + * device will see this event enabled. Currently there is no option of + * disabling an event type or code at kernel-level. + */ + +/** + * @defgroup misc Miscellaneous helper functions + * + * Functions for printing or querying event ranges. The list of names is + * compiled into libevdev and is independent of the run-time kernel. + * Likewise, the max for each event type is compiled in and does not check + * the kernel at run-time. + */ + +/** + * @defgroup events Event handling + * + * Functions to handle events and fetch the current state of the event. + * libevdev updates its internal state as the event is processed and forwarded + * to the caller. Thus, the libevdev state of the device should always be identical + * to the caller's state. It may however lag behind the actual state of the device. + */ + +/** + * @ingroup init + * + * Opaque struct representing an evdev device. + */ +struct libevdev; + +/** + * @ingroup events + */ +enum libevdev_read_flag { + LIBEVDEV_READ_FLAG_SYNC = 1, /**< Process data in sync mode */ + LIBEVDEV_READ_FLAG_NORMAL = 2, /**< Process data in normal mode */ + LIBEVDEV_READ_FLAG_FORCE_SYNC = 4, /**< Pretend the next event is a SYN_DROPPED and + require the caller to sync */ + LIBEVDEV_READ_FLAG_BLOCKING = 8 /**< The fd is not in O_NONBLOCK and a read may block */ +}; + +/** + * @ingroup init + * + * Initialize a new libevdev device. This function only allocates the + * required memory and initializes the struct to sane default values. + * To actually hook up the device to a kernel device, use + * libevdev_set_fd(). + * + * Memory allocated through libevdev_new() must be released by the + * caller with libevdev_free(). + * + * @see libevdev_set_fd + * @see libevdev_free + */ +struct libevdev* libevdev_new(void); + +/** + * @ingroup init + * + * Initialize a new libevdev device from the given fd. + * + * This is a shortcut for + * + * @code + * int err; + * struct libevdev *dev = libevdev_new(); + * err = libevdev_set_fd(dev, fd); + * @endcode + * + * @param fd A file descriptor to the device in O_RDWR or O_RDONLY mode. + * @param[out] dev The newly initialized evdev device. + * + * @return On success, 0 is returned and dev is set to the newly + * allocated struct. On failure, a negative errno is returned and the value + * of dev is undefined. + * + * @see libevdev_free + */ +int libevdev_new_from_fd(int fd, struct libevdev **dev); + +/** + * @ingroup init + * + * Clean up and free the libevdev struct. After completion, the struct + * libevdev is invalid and must not be used. + * + * Note that calling libevdev_free() does not close the file descriptor + * currently associated with this instance. + * + * @param dev The evdev device + * + * @note This function may be called before libevdev_set_fd(). + */ +void libevdev_free(struct libevdev *dev); + +/** + * @ingroup logging + */ +enum libevdev_log_priority { + LIBEVDEV_LOG_ERROR = 10, /**< critical errors and application bugs */ + LIBEVDEV_LOG_INFO = 20, /**< informational messages */ + LIBEVDEV_LOG_DEBUG = 30 /**< debug information */ +}; + +/** + * @ingroup logging + * + * Logging function called by library-internal logging. + * This function is expected to treat its input like printf would. + * + * @param priority Log priority of this message + * @param data User-supplied data pointer (see libevdev_set_log_function()) + * @param file libevdev source code file generating this message + * @param line libevdev source code line generating this message + * @param func libevdev source code function generating this message + * @param format printf-style format string + * @param args List of arguments + * + * @see libevdev_set_log_function + */ +typedef void (*libevdev_log_func_t)(enum libevdev_log_priority priority, + void *data, + const char *file, int line, + const char *func, + const char *format, va_list args) + LIBEVDEV_ATTRIBUTE_PRINTF(6, 0); + +/** + * @ingroup logging + * + * Set a printf-style logging handler for library-internal logging. The default + * logging function is to stdout. + * + * @note The global log handler is only called if no context-specific log + * handler has been set with libevdev_set_device_log_function(). + * + * @param logfunc The logging function for this device. If NULL, the current + * logging function is unset and no logging is performed. + * @param data User-specific data passed to the log handler. + * + * @note This function may be called before libevdev_set_fd(). + * + * @deprecated Use per-context logging instead, see + * libevdev_set_device_log_function(). + */ +void libevdev_set_log_function(libevdev_log_func_t logfunc, void *data); + +/** + * @ingroup logging + * + * Define the minimum level to be printed to the log handler. + * Messages higher than this level are printed, others are discarded. This + * is a global setting and applies to any future logging messages. + * + * @param priority Minimum priority to be printed to the log. + * + * @deprecated Use per-context logging instead, see + * libevdev_set_device_log_function(). + */ +void libevdev_set_log_priority(enum libevdev_log_priority priority); + +/** + * @ingroup logging + * + * Return the current log priority level. Messages higher than this level + * are printed, others are discarded. This is a global setting. + * + * @return the current log level + * + * @deprecated Use per-context logging instead, see + * libevdev_set_device_log_function(). + */ +enum libevdev_log_priority libevdev_get_log_priority(void); + +/** + * @ingroup logging + * + * Logging function called by library-internal logging for a specific + * libevdev context. This function is expected to treat its input like + * printf would. + * + * @param dev The evdev device + * @param priority Log priority of this message + * @param data User-supplied data pointer (see libevdev_set_log_function()) + * @param file libevdev source code file generating this message + * @param line libevdev source code line generating this message + * @param func libevdev source code function generating this message + * @param format printf-style format string + * @param args List of arguments + * + * @see libevdev_set_log_function + * @since 1.3 + */ +typedef void (*libevdev_device_log_func_t)(const struct libevdev *dev, + enum libevdev_log_priority priority, + void *data, + const char *file, int line, + const char *func, + const char *format, va_list args) + LIBEVDEV_ATTRIBUTE_PRINTF(7, 0); + +/** + * @ingroup logging + * + * Set a printf-style logging handler for library-internal logging for this + * device context. The default logging function is NULL, i.e. the global log + * handler is invoked. If a context-specific log handler is set, the global + * log handler is not invoked for this device. + * + * @note This log function applies for this device context only, even if + * another context exists for the same fd. + * + * @param dev The evdev device + * @param logfunc The logging function for this device. If NULL, the current + * logging function is unset and logging falls back to the global log + * handler, if any. + * @param priority Minimum priority to be printed to the log. + * @param data User-specific data passed to the log handler. + * + * @note This function may be called before libevdev_set_fd(). + * @since 1.3 + */ +void libevdev_set_device_log_function(struct libevdev *dev, + libevdev_device_log_func_t logfunc, + enum libevdev_log_priority priority, + void *data); + +/** + * @ingroup init + */ +enum libevdev_grab_mode { + LIBEVDEV_GRAB = 3, /**< Grab the device if not currently grabbed */ + LIBEVDEV_UNGRAB = 4 /**< Ungrab the device if currently grabbed */ +}; + +/** + * @ingroup init + * + * Grab or ungrab the device through a kernel EVIOCGRAB. This prevents other + * clients (including kernel-internal ones such as rfkill) from receiving + * events from this device. + * + * This is generally a bad idea. Don't do this. + * + * Grabbing an already grabbed device, or ungrabbing an ungrabbed device is + * a noop and always succeeds. + * + * A grab is an operation tied to a file descriptor, not a device. If a + * client changes the file descriptor with libevdev_change_fd(), it must + * also re-issue a grab with libevdev_grab(). + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param grab If true, grab the device. Otherwise ungrab the device. + * + * @return 0 if the device was successfully grabbed or ungrabbed, or a + * negative errno in case of failure. + */ +int libevdev_grab(struct libevdev *dev, enum libevdev_grab_mode grab); + +/** + * @ingroup init + * + * Set the fd for this struct and initialize internal data. + * The fd must be in O_RDONLY or O_RDWR mode. + * + * This function may only be called once per device. If the device changed and + * you need to re-read a device, use libevdev_free() and libevdev_new(). If + * you need to change the fd after closing and re-opening the same device, use + * libevdev_change_fd(). + * + * A caller should ensure that any events currently pending on the fd are + * drained before the file descriptor is passed to libevdev for + * initialization. Due to how the kernel's ioctl handling works, the initial + * device state will reflect the current device state *after* applying all + * events currently pending on the fd. Thus, if the fd is not drained, the + * state visible to the caller will be inconsistent with the events + * immediately available on the device. This does not affect state-less + * events like EV_REL. + * + * Unless otherwise specified, libevdev function behavior is undefined until + * a successful call to libevdev_set_fd(). + * + * @param dev The evdev device + * @param fd The file descriptor for the device + * + * @return 0 on success, or a negative errno on failure + * + * @see libevdev_change_fd + * @see libevdev_new + * @see libevdev_free + */ +int libevdev_set_fd(struct libevdev* dev, int fd); + +/** + * @ingroup init + * + * Change the fd for this device, without re-reading the actual device. If the fd + * changes after initializing the device, for example after a VT-switch in the + * X.org X server, this function updates the internal fd to the newly opened. + * No check is made that new fd points to the same device. If the device has + * changed, libevdev's behavior is undefined. + * + * libevdev does not sync itself after changing the fd and keeps the current + * device state. Use libevdev_next_event with the + * @ref LIBEVDEV_READ_FLAG_FORCE_SYNC flag to force a re-sync. + * + * The example code below illustrates how to force a re-sync of the + * library-internal state. Note that this code doesn't handle the events in + * the caller, it merely forces an update of the internal library state. + * @code + * struct input_event ev; + * libevdev_change_fd(dev, new_fd); + * libevdev_next_event(dev, LIBEVDEV_READ_FLAG_FORCE_SYNC, &ev); + * while (libevdev_next_event(dev, LIBEVDEV_READ_FLAG_SYNC, &ev) == LIBEVDEV_READ_STATUS_SYNC) + * ; // noop + * @endcode + * + * The fd may be open in O_RDONLY or O_RDWR. + * + * After changing the fd, the device is assumed ungrabbed and a caller must + * call libevdev_grab() again. + * + * It is an error to call this function before calling libevdev_set_fd(). + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param fd The new fd + * + * @return 0 on success, or -1 on failure. + * + * @see libevdev_set_fd + */ +int libevdev_change_fd(struct libevdev* dev, int fd); + +/** + * @ingroup init + * + * @param dev The evdev device + * + * @return The previously set fd, or -1 if none had been set previously. + * @note This function may be called before libevdev_set_fd(). + */ +int libevdev_get_fd(const struct libevdev* dev); + +/** + * @ingroup events + */ +enum libevdev_read_status { + /** + * libevdev_next_event() has finished without an error + * and an event is available for processing. + * + * @see libevdev_next_event + */ + LIBEVDEV_READ_STATUS_SUCCESS = 0, + /** + * Depending on the libevdev_next_event() read flag: + * * libevdev received a SYN_DROPPED from the device, and the caller should + * now resync the device, or, + * * an event has been read in sync mode. + * + * @see libevdev_next_event + */ + LIBEVDEV_READ_STATUS_SYNC = 1 +}; + +/** + * @ingroup events + * + * Get the next event from the device. This function operates in two different + * modes: normal mode or sync mode. + * + * In normal mode (when flags has @ref LIBEVDEV_READ_FLAG_NORMAL set), this + * function returns @ref LIBEVDEV_READ_STATUS_SUCCESS and returns the event + * in the argument @p ev. If no events are available at this + * time, it returns -EAGAIN and ev is undefined. + * + * If the current event is an EV_SYN SYN_DROPPED event, this function returns + * @ref LIBEVDEV_READ_STATUS_SYNC and ev is set to the EV_SYN event. + * The caller should now call this function with the + * @ref LIBEVDEV_READ_FLAG_SYNC flag set, to get the set of events that make up the + * device state delta. This function returns @ref LIBEVDEV_READ_STATUS_SYNC for + * each event part of that delta, until it returns -EAGAIN once all events + * have been synced. For more details on what libevdev does to sync after a + * SYN_DROPPED event, see @ref syn_dropped. + * + * If a device needs to be synced by the caller but the caller does not call + * with the @ref LIBEVDEV_READ_FLAG_SYNC flag set, all events from the diff are + * dropped after libevdev updates its internal state and event processing + * continues as normal. Note that the current slot and the state of touch + * points may have updated during the SYN_DROPPED event, it is strongly + * recommended that a caller ignoring all sync events calls + * libevdev_get_current_slot() and checks the ABS_MT_TRACKING_ID values for + * all slots. + * + * If a device has changed state without events being enqueued in libevdev, + * e.g. after changing the file descriptor, use the @ref + * LIBEVDEV_READ_FLAG_FORCE_SYNC flag. This triggers an internal sync of the + * device and libevdev_next_event() returns @ref LIBEVDEV_READ_STATUS_SYNC. + * Any state changes are available as events as described above. If + * @ref LIBEVDEV_READ_FLAG_FORCE_SYNC is set, the value of ev is undefined. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param flags Set of flags to determine behaviour. If @ref LIBEVDEV_READ_FLAG_NORMAL + * is set, the next event is read in normal mode. If @ref LIBEVDEV_READ_FLAG_SYNC is + * set, the next event is read in sync mode. + * @param ev On success, set to the current event. + * @return On failure, a negative errno is returned. + * @retval LIBEVDEV_READ_STATUS_SUCCESS One or more events were read of the + * device and ev points to the next event in the queue + * @retval -EAGAIN No events are currently available on the device + * @retval LIBEVDEV_READ_STATUS_SYNC A SYN_DROPPED event was received, or a + * synced event was returned and ev points to the SYN_DROPPED event + * + * @note This function is signal-safe. + */ +int libevdev_next_event(struct libevdev *dev, unsigned int flags, struct input_event *ev); + +/** + * @ingroup events + * + * Check if there are events waiting for us. This function does not read an + * event off the fd and may not access the fd at all. If there are events + * queued internally this function will return non-zero. If the internal + * queue is empty, this function will poll the file descriptor for data. + * + * This is a convenience function for simple processes, most complex programs + * are expected to use select(2) or poll(2) on the file descriptor. The kernel + * guarantees that if data is available, it is a multiple of sizeof(struct + * input_event), and thus calling libevdev_next_event() when select(2) or + * poll(2) return is safe. You do not need libevdev_has_event_pending() if + * you're using select(2) or poll(2). + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @return On failure, a negative errno is returned. + * @retval 0 No event is currently available + * @retval 1 One or more events are available on the fd + * + * @note This function is signal-safe. + */ +int libevdev_has_event_pending(struct libevdev *dev); + +/** + * @ingroup bits + * + * Retrieve the device's name, either as set by the caller or as read from + * the kernel. The string returned is valid until libevdev_free() or until + * libevdev_set_name(), whichever comes earlier. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device name as read off the kernel device. The name is never + * NULL but it may be the empty string. + * + * @note This function is signal-safe. + */ +const char* libevdev_get_name(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * Change the device's name as returned by libevdev_get_name(). This + * function destroys the string previously returned by libevdev_get_name(), + * a caller must take care that no references are kept. + * + * @param dev The evdev device + * @param name The new, non-NULL, name to assign to this device. + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. + */ +void libevdev_set_name(struct libevdev *dev, const char *name); + +/** + * @ingroup bits + * + * Retrieve the device's physical location, either as set by the caller or + * as read from the kernel. The string returned is valid until + * libevdev_free() or until libevdev_set_phys(), whichever comes earlier. + * + * Virtual devices such as uinput devices have no phys location. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The physical location of this device, or NULL if there is none + * + * @note This function is signal safe. + */ +const char * libevdev_get_phys(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * Change the device's physical location as returned by libevdev_get_phys(). + * This function destroys the string previously returned by + * libevdev_get_phys(), a caller must take care that no references are kept. + * + * @param dev The evdev device + * @param phys The new phys to assign to this device. + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. + */ +void libevdev_set_phys(struct libevdev *dev, const char *phys); + +/** + * @ingroup bits + * + * Retrieve the device's unique identifier, either as set by the caller or + * as read from the kernel. The string returned is valid until + * libevdev_free() or until libevdev_set_uniq(), whichever comes earlier. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The unique identifier for this device, or NULL if there is none + * + * @note This function is signal safe. + */ +const char * libevdev_get_uniq(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * Change the device's unique identifier as returned by libevdev_get_uniq(). + * This function destroys the string previously returned by + * libevdev_get_uniq(), a caller must take care that no references are kept. + * + * @param dev The evdev device + * @param uniq The new uniq to assign to this device. + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. + */ +void libevdev_set_uniq(struct libevdev *dev, const char *uniq); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device's product ID + * + * @note This function is signal-safe. + */ +int libevdev_get_id_product(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param product_id The product ID to assign to this device + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. Even though + * the function accepts an int for product_id the value is truncated at 16 + * bits. + */ +void libevdev_set_id_product(struct libevdev *dev, int product_id); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device's vendor ID + * + * @note This function is signal-safe. + */ +int libevdev_get_id_vendor(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param vendor_id The vendor ID to assign to this device + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. Even though + * the function accepts an int for vendor_id the value is truncated at 16 + * bits. + */ +void libevdev_set_id_vendor(struct libevdev *dev, int vendor_id); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device's bus type + * + * @note This function is signal-safe. + */ +int libevdev_get_id_bustype(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param bustype The bustype to assign to this device + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. Even though + * the function accepts an int for bustype the value is truncated at 16 + * bits. + */ +void libevdev_set_id_bustype(struct libevdev *dev, int bustype); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device's firmware version + * + * @note This function is signal-safe. + */ +int libevdev_get_id_version(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param version The version to assign to this device + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. Even though + * the function accepts an int for version the value is truncated at 16 + * bits. + */ +void libevdev_set_id_version(struct libevdev *dev, int version); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The driver version for this device + * + * @note This function is signal-safe. + */ +int libevdev_get_driver_version(const struct libevdev *dev); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param prop The input property to query for, one of INPUT_PROP_... + * + * @return 1 if the device provides this input property, or 0 otherwise. + * + * @note This function is signal-safe + */ +int libevdev_has_property(const struct libevdev *dev, unsigned int prop); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param prop The input property to enable, one of INPUT_PROP_... + * + * @return 0 on success or -1 on failure + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. + */ +int libevdev_enable_property(struct libevdev *dev, unsigned int prop); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param prop The input property to disable, one of INPUT_PROP_... + * + * @return 0 on success or -1 on failure + */ +int libevdev_disable_property(struct libevdev *dev, unsigned int prop); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to query for, one of EV_SYN, EV_REL, etc. + * + * @return 1 if the device supports this event type, or 0 otherwise. + * + * @note This function is signal-safe. + */ +int libevdev_has_event_type(const struct libevdev *dev, unsigned int type); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to query for, one of ABS_X, REL_X, etc. + * + * @return 1 if the device supports this event type and code, or 0 otherwise. + * + * @note This function is signal-safe. + */ +int libevdev_has_event_code(const struct libevdev *dev, unsigned int type, unsigned int code); + +/** + * @ingroup bits + * + * Get the minimum axis value for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis minimum for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_minimum(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the maximum axis value for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis maximum for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_maximum(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the axis fuzz for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis fuzz for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_fuzz(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the axis flat for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis flat for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_flat(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the axis resolution for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis resolution for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_resolution(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the axis info for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return The input_absinfo for the given code, or NULL if the device does + * not support this event code. + * + * @note This function is signal-safe. + */ +const struct input_absinfo* libevdev_get_abs_info(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Behaviour of this function is undefined if the device does not provide + * the event. + * + * If the device supports ABS_MT_SLOT, the value returned for any ABS_MT_* + * event code is undefined. Use libevdev_get_slot_value() instead. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to query for, one of ABS_X, REL_X, etc. + * + * @return The current value of the event. + * + * @note This function is signal-safe. + * @note The value for ABS_MT_ events is undefined, use + * libevdev_get_slot_value() instead + * + * @see libevdev_get_slot_value + */ +int libevdev_get_event_value(const struct libevdev *dev, unsigned int type, unsigned int code); + +/** + * @ingroup kernel + * + * Set the value for a given event type and code. This only makes sense for + * some event types, e.g. setting the value for EV_REL is pointless. + * + * This is a local modification only affecting only this representation of + * this device. A future call to libevdev_get_event_value() will return this + * value, unless the value was overwritten by an event. + * + * If the device supports ABS_MT_SLOT, the value set for any ABS_MT_* + * event code is the value of the currently active slot. You should use + * libevdev_set_slot_value() instead. + * + * If the device supports ABS_MT_SLOT and the type is EV_ABS and the code is + * ABS_MT_SLOT, the value must be a positive number less then the number of + * slots on the device. Otherwise, libevdev_set_event_value() returns -1. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to set the value for, one of ABS_X, LED_NUML, etc. + * @param value The new value to set + * + * @return 0 on success, or -1 on failure. + * @retval -1 + * - the device does not have the event type or + * - code enabled, or the code is outside the, or + * - the code is outside the allowed limits for the given type, or + * - the type cannot be set, or + * - the value is not permitted for the given code. + * + * @see libevdev_set_slot_value + * @see libevdev_get_event_value + */ +int libevdev_set_event_value(struct libevdev *dev, unsigned int type, unsigned int code, int value); + +/** + * @ingroup bits + * + * Fetch the current value of the event type. This is a shortcut for + * + * @code + * if (libevdev_has_event_type(dev, t) && libevdev_has_event_code(dev, t, c)) + * val = libevdev_get_event_value(dev, t, c); + * @endcode + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to query for, one of ABS_X, REL_X, etc. + * @param[out] value The current value of this axis returned. + * + * @return If the device supports this event type and code, the return value is + * non-zero and value is set to the current value of this axis. Otherwise, + * 0 is returned and value is unmodified. + * + * @note This function is signal-safe. + * @note The value for ABS_MT_ events is undefined, use + * libevdev_fetch_slot_value() instead + * + * @see libevdev_fetch_slot_value + */ +int libevdev_fetch_event_value(const struct libevdev *dev, unsigned int type, unsigned int code, int *value); + +/** + * @ingroup mt + * + * Return the current value of the code for the given slot. + * + * The return value is undefined for a slot exceeding the available slots on + * the device, for a code that is not in the permitted ABS_MT range or for a + * device that does not have slots. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param slot The numerical slot number, must be smaller than the total number + * of slots on this device + * @param code The event code to query for, one of ABS_MT_POSITION_X, etc. + * + * @note This function is signal-safe. + * @note The value for events other than ABS_MT_ is undefined, use + * libevdev_fetch_value() instead + * + * @see libevdev_get_event_value + */ +int libevdev_get_slot_value(const struct libevdev *dev, unsigned int slot, unsigned int code); + +/** + * @ingroup kernel + * + * Set the value for a given code for the given slot. + * + * This is a local modification only affecting only this representation of + * this device. A future call to libevdev_get_slot_value() will return this + * value, unless the value was overwritten by an event. + * + * This function does not set event values for axes outside the ABS_MT range, + * use libevdev_set_event_value() instead. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param slot The numerical slot number, must be smaller than the total number + * of slots on this device + * @param code The event code to set the value for, one of ABS_MT_POSITION_X, etc. + * @param value The new value to set + * + * @return 0 on success, or -1 on failure. + * @retval -1 + * - the device does not have the event code enabled, or + * - the code is outside the allowed limits for multitouch events, or + * - the slot number is outside the limits for this device, or + * - the device does not support multitouch events. + * + * @see libevdev_set_event_value + * @see libevdev_get_slot_value + */ +int libevdev_set_slot_value(struct libevdev *dev, unsigned int slot, unsigned int code, int value); + +/** + * @ingroup mt + * + * Fetch the current value of the code for the given slot. This is a shortcut for + * + * @code + * if (libevdev_has_event_type(dev, EV_ABS) && + * libevdev_has_event_code(dev, EV_ABS, c) && + * slot < device->number_of_slots) + * val = libevdev_get_slot_value(dev, slot, c); + * @endcode + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param slot The numerical slot number, must be smaller than the total number + * of slots on this * device + * @param[out] value The current value of this axis returned. + * + * @param code The event code to query for, one of ABS_MT_POSITION_X, etc. + * @return If the device supports this event code, the return value is + * non-zero and value is set to the current value of this axis. Otherwise, or + * if the event code is not an ABS_MT_* event code, 0 is returned and value + * is unmodified. + * + * @note This function is signal-safe. + */ +int libevdev_fetch_slot_value(const struct libevdev *dev, unsigned int slot, unsigned int code, int *value); + +/** + * @ingroup mt + * + * Get the number of slots supported by this device. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The number of slots supported, or -1 if the device does not provide + * any slots + * + * @note A device may provide ABS_MT_SLOT but a total number of 0 slots. Hence + * the return value of -1 for "device does not provide slots at all" + */ +int libevdev_get_num_slots(const struct libevdev *dev); + +/** + * @ingroup mt + * + * Get the currently active slot. This may differ from the value + * an ioctl may return at this time as events may have been read off the fd + * since changing the slot value but those events are still in the buffer + * waiting to be processed. The returned value is the value a caller would + * see if it were to process events manually one-by-one. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return the currently active slot (logically) + * + * @note This function is signal-safe. + */ +int libevdev_get_current_slot(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * Change the minimum for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new minimum for this axis + */ +void libevdev_set_abs_minimum(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the maximum for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new maxium for this axis + */ +void libevdev_set_abs_maximum(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the fuzz for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new fuzz for this axis + */ +void libevdev_set_abs_fuzz(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the flat for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new flat for this axis + */ +void libevdev_set_abs_flat(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the resolution for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new axis resolution + */ +void libevdev_set_abs_resolution(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the abs info for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param abs The new absolute axis data (min, max, fuzz, flat, resolution) + */ +void libevdev_set_abs_info(struct libevdev *dev, unsigned int code, const struct input_absinfo *abs); + +/** + * @ingroup kernel + * + * Forcibly enable an event type on this device, even if the underlying + * device does not support it. While this cannot make the device actually + * report such events, it will now return true for libevdev_has_event_type(). + * + * This is a local modification only affecting only this representation of + * this device. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to enable (EV_ABS, EV_KEY, ...) + * + * @return 0 on success or -1 otherwise + * + * @see libevdev_has_event_type + */ +int libevdev_enable_event_type(struct libevdev *dev, unsigned int type); + +/** + * @ingroup kernel + * + * Forcibly disable an event type on this device, even if the underlying + * device provides it. This effectively mutes the respective set of + * events. libevdev will filter any events matching this type and none will + * reach the caller. libevdev_has_event_type() will return false for this + * type. + * + * In most cases, a caller likely only wants to disable a single code, not + * the whole type. Use libevdev_disable_event_code() for that. + * + * Disabling EV_SYN will not work. Don't shoot yourself in the foot. + * It hurts. + * + * This is a local modification only affecting only this representation of + * this device. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to disable (EV_ABS, EV_KEY, ...) + * + * @return 0 on success or -1 otherwise + * + * @see libevdev_has_event_type + * @see libevdev_disable_event_type + */ +int libevdev_disable_event_type(struct libevdev *dev, unsigned int type); + +/** + * @ingroup kernel + * + * Forcibly enable an event code on this device, even if the underlying + * device does not support it. While this cannot make the device actually + * report such events, it will now return true for libevdev_has_event_code(). + * + * The last argument depends on the type and code: + * - If type is EV_ABS, data must be a pointer to a struct input_absinfo + * containing the data for this axis. + * - If type is EV_REP, data must be a pointer to a int containing the data + * for this axis + * - For all other types, the argument must be NULL. + * + * This function calls libevdev_enable_event_type() if necessary. + * + * This is a local modification only affecting only this representation of + * this device. + * + * If this function is called with a type of EV_ABS and EV_REP on a device + * that already has the given event code enabled, the values in data + * overwrite the previous values. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to enable (EV_ABS, EV_KEY, ...) + * @param code The event code to enable (ABS_X, REL_X, etc.) + * @param data If type is EV_ABS, data points to a struct input_absinfo. If type is EV_REP, data + * points to an integer. Otherwise, data must be NULL. + * + * @return 0 on success or -1 otherwise + * + * @see libevdev_enable_event_type + */ +int libevdev_enable_event_code(struct libevdev *dev, unsigned int type, unsigned int code, const void *data); + +/** + * @ingroup kernel + * + * Forcibly disable an event code on this device, even if the underlying + * device provides it. This effectively mutes the respective set of + * events. libevdev will filter any events matching this type and code and + * none will reach the caller. libevdev_has_event_code() will return false for + * this code. + * + * Disabling all event codes for a given type will not disable the event + * type. Use libevdev_disable_event_type() for that. + * + * This is a local modification only affecting only this representation of + * this device. + * + * Disabling codes of type EV_SYN will not work. Don't shoot yourself in the + * foot. It hurts. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to disable (EV_ABS, EV_KEY, ...) + * @param code The event code to disable (ABS_X, REL_X, etc.) + * + * @return 0 on success or -1 otherwise + * + * @see libevdev_has_event_code + * @see libevdev_disable_event_type + */ +int libevdev_disable_event_code(struct libevdev *dev, unsigned int type, unsigned int code); + +/** + * @ingroup kernel + * + * Set the device's EV_ABS axis to the value defined in the abs + * parameter. This will be written to the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to modify, one of ABS_X, ABS_Y, etc. + * @param abs Axis info to set the kernel axis to + * + * @return 0 on success, or a negative errno on failure + * + * @see libevdev_enable_event_code + */ +int libevdev_kernel_set_abs_info(struct libevdev *dev, unsigned int code, const struct input_absinfo *abs); + +/** + * @ingroup kernel + */ +enum libevdev_led_value { + LIBEVDEV_LED_ON = 3, /**< Turn the LED on */ + LIBEVDEV_LED_OFF = 4 /**< Turn the LED off */ +}; + +/** + * @ingroup kernel + * + * Turn an LED on or off. Convenience function, if you need to modify multiple + * LEDs simultaneously, use libevdev_kernel_set_led_values() instead. + * + * @note enabling an LED requires write permissions on the device's file descriptor. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_LED event code to modify, one of LED_NUML, LED_CAPSL, ... + * @param value Specifies whether to turn the LED on or off + * @return 0 on success, or a negative errno on failure + */ +int libevdev_kernel_set_led_value(struct libevdev *dev, unsigned int code, enum libevdev_led_value value); + +/** + * @ingroup kernel + * + * Turn multiple LEDs on or off simultaneously. This function expects a pair + * of LED codes and values to set them to, terminated by a -1. For example, to + * switch the NumLock LED on but the CapsLock LED off, use: + * + * @code + * libevdev_kernel_set_led_values(dev, LED_NUML, LIBEVDEV_LED_ON, + * LED_CAPSL, LIBEVDEV_LED_OFF, + * -1); + * @endcode + * + * If any LED code or value is invalid, this function returns -EINVAL and no + * LEDs are modified. + * + * @note enabling an LED requires write permissions on the device's file descriptor. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param ... A pair of LED_* event codes and libevdev_led_value_t, followed by + * -1 to terminate the list. + * @return 0 on success, or a negative errno on failure + */ +int libevdev_kernel_set_led_values(struct libevdev *dev, ...); + +/** + * @ingroup kernel + * + * Set the clock ID to be used for timestamps. Further events from this device + * will report an event time based on the given clock. + * + * This is a modification only affecting this representation of + * this device. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param clockid The clock to use for future events. Permitted values + * are CLOCK_MONOTONIC and CLOCK_REALTIME (the default). + * @return 0 on success, or a negative errno on failure + */ +int libevdev_set_clock_id(struct libevdev *dev, int clockid); + +/** + * @ingroup misc + * + * Helper function to check if an event is of a specific type. This is + * virtually the same as: + * + * ev->type == type + * + * with the exception that some sanity checks are performed to ensure type + * is valid. + * + * @note The ranges for types are compiled into libevdev. If the kernel + * changes the max value, libevdev will not automatically pick these up. + * + * @param ev The input event to check + * @param type Input event type to compare the event against (EV_REL, EV_ABS, + * etc.) + * + * @return 1 if the event type matches the given type, 0 otherwise (or if + * type is invalid) + */ +int libevdev_event_is_type(const struct input_event *ev, unsigned int type); + +/** + * @ingroup misc + * + * Helper function to check if an event is of a specific type and code. This + * is virtually the same as: + * + * ev->type == type && ev->code == code + * + * with the exception that some sanity checks are performed to ensure type and + * code are valid. + * + * @note The ranges for types and codes are compiled into libevdev. If the kernel + * changes the max value, libevdev will not automatically pick these up. + * + * @param ev The input event to check + * @param type Input event type to compare the event against (EV_REL, EV_ABS, + * etc.) + * @param code Input event code to compare the event against (ABS_X, REL_X, + * etc.) + * + * @return 1 if the event type matches the given type and code, 0 otherwise + * (or if type/code are invalid) + */ +int libevdev_event_is_code(const struct input_event *ev, unsigned int type, unsigned int code); + +/** + * @ingroup misc + * + * @param type The event type to return the name for. + * + * @return The name of the given event type (e.g. EV_ABS) or NULL for an + * invalid type + * + * @note The list of names is compiled into libevdev. If the kernel adds new + * defines for new event types, libevdev will not automatically pick these up. + */ +const char * libevdev_event_type_get_name(unsigned int type); +/** + * @ingroup misc + * + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to return the name for (e.g. ABS_X) + * + * @return The name of the given event code (e.g. ABS_X) or NULL for an + * invalid type or code + * + * @note The list of names is compiled into libevdev. If the kernel adds new + * defines for new event codes, libevdev will not automatically pick these up. + */ +const char * libevdev_event_code_get_name(unsigned int type, unsigned int code); + +/** + * @ingroup misc + * + * This function resolves the event value for a code. + * + * For almost all event codes this will return NULL as the value is just a + * numerical value. As of kernel 4.17, the only event code that will return + * a non-NULL value is EV_ABS/ABS_MT_TOOL_TYPE. + * + * @param type The event type for the value to query (EV_ABS, etc.) + * @param code The event code for the value to query (e.g. ABS_MT_TOOL_TYPE) + * @param value The event value to return the name for (e.g. MT_TOOL_PALM) + * + * @return The name of the given event value (e.g. MT_TOOL_PALM) or NULL for + * an invalid type or code or NULL for an axis that has numerical values + * only. + * + * @note The list of names is compiled into libevdev. If the kernel adds new + * defines for new event values, libevdev will not automatically pick these up. + */ +const char * libevdev_event_value_get_name(unsigned int type, + unsigned int code, + int value); +/** + * @ingroup misc + * + * @param prop The input prop to return the name for (e.g. INPUT_PROP_BUTTONPAD) + * + * @return The name of the given input prop (e.g. INPUT_PROP_BUTTONPAD) or NULL for an + * invalid property + * + * @note The list of names is compiled into libevdev. If the kernel adds new + * defines for new properties libevdev will not automatically pick these up. + * @note On older kernels input properties may not be defined and + * libevdev_property_get_name() will always return NULL + */ +const char* libevdev_property_get_name(unsigned int prop); + +/** + * @ingroup misc + * + * @param type The event type to return the maximum for (EV_ABS, EV_REL, etc.). No max is defined for + * EV_SYN. + * + * @return The max value defined for the given event type, e.g. ABS_MAX for a type of EV_ABS, or -1 + * for an invalid type. + * + * @note The max value is compiled into libevdev. If the kernel changes the + * max value, libevdev will not automatically pick these up. + */ +int libevdev_event_type_get_max(unsigned int type); + +/** + * @ingroup misc + * + * Look up an event-type by its name. Event-types start with "EV_" followed by + * the name (eg., "EV_ABS"). The "EV_" prefix must be included in the name. It + * returns the constant assigned to the event-type or -1 if not found. + * + * @param name A non-NULL string describing an input-event type ("EV_KEY", + * "EV_ABS", ...), zero-terminated. + * + * @return The given type constant for the passed name or -1 if not found. + * + * @note EV_MAX is also recognized. + */ +int libevdev_event_type_from_name(const char *name); + +/** + * @ingroup misc + * + * Look up an event-type by its name. Event-types start with "EV_" followed by + * the name (eg., "EV_ABS"). The "EV_" prefix must be included in the name. It + * returns the constant assigned to the event-type or -1 if not found. + * + * @param name A non-NULL string describing an input-event type ("EV_KEY", + * "EV_ABS", ...). + * @param len The length of the passed string excluding any terminating 0 + * character. + * + * @return The given type constant for the passed name or -1 if not found. + * + * @note EV_MAX is also recognized. + */ +int libevdev_event_type_from_name_n(const char *name, size_t len); + +/** + * @ingroup misc + * + * Look up an event code by its type and name. Event codes start with a fixed + * prefix followed by their name (eg., "ABS_X"). The prefix must be included in + * the name. It returns the constant assigned to the event code or -1 if not + * found. + * + * You have to pass the event type where to look for the name. For instance, to + * resolve "ABS_X" you need to pass EV_ABS as type and "ABS_X" as string. + * Supported event codes are codes starting with SYN_, KEY_, BTN_, REL_, ABS_, + * MSC_, SND_, SW_, LED_, REP_, FF_. + * + * @param type The event type (EV_* constant) where to look for the name. + * @param name A non-NULL string describing an input-event code ("KEY_A", + * "ABS_X", "BTN_Y", ...), zero-terminated. + * + * @return The given code constant for the passed name or -1 if not found. + */ +int libevdev_event_code_from_name(unsigned int type, const char *name); + +/** + * @ingroup misc + * + * Look up an event code by its type and name. Event codes start with a fixed + * prefix followed by their name (eg., "ABS_X"). The prefix must be included in + * the name. It returns the constant assigned to the event code or -1 if not + * found. + * + * You have to pass the event type where to look for the name. For instance, to + * resolve "ABS_X" you need to pass EV_ABS as type and "ABS_X" as string. + * Supported event codes are codes starting with SYN_, KEY_, BTN_, REL_, ABS_, + * MSC_, SND_, SW_, LED_, REP_, FF_. + * + * @param type The event type (EV_* constant) where to look for the name. + * @param name A non-NULL string describing an input-event code ("KEY_A", + * "ABS_X", "BTN_Y", ...). + * @param len The length of the string in @p name excluding any terminating 0 + * character. + * + * @return The given code constant for the name or -1 if not found. + */ +int libevdev_event_code_from_name_n(unsigned int type, const char *name, + size_t len); + +/** + * @ingroup misc + * + * Look up an event value by its type, code and name. Event values start + * with a fixed prefix followed by their name (eg., "MT_TOOL_PALM"). The + * prefix must be included in the name. It returns the constant assigned + * to the event code or -1 if not found. + * + * You have to pass the event type and code where to look for the name. For + * instance, to resolve "MT_TOOL_PALM" you need to pass EV_ABS as type, + * ABS_MT_TOOL_TYPE as code and "MT_TOOL_PALM" as string. + * + * As of kernel 4.17, only EV_ABS/ABS_MT_TOOL_TYPE support name resolution. + * + * @param type The event type (EV_* constant) where to look for the name. + * @param code The event code (ABS_* constant) where to look for the name. + * @param name A non-NULL string describing an input-event value + * ("MT_TOOL_TYPE", ...) + * + * @return The given value constant for the name or -1 if not found. + */ +int libevdev_event_value_from_name(unsigned int type, unsigned int code, + const char *name); + +/** + * @ingroup misc + * + * Look up an event type for a event code name. For example, the name + * "ABS_Y" returns EV_ABS. For the lookup to succeed, the name must be + * unique, which is the case for all defines as of kernel 5.0 and likely to + * be the case in the future. + * + * This is equivalent to libevdev_event_type_from_name() but takes the code + * name instead of the type name. + * + * @param name A non-NULL string describing an input-event value + * ("ABS_X", "REL_Y", "KEY_A", ...) + * + * @return The given event code for the name or -1 if not found. + */ +int +libevdev_event_type_from_code_name(const char *name); + +/** + * @ingroup misc + * + * Look up an event type for a event code name. For example, the name + * "ABS_Y" returns EV_ABS. For the lookup to succeed, the name must be + * unique, which is the case for all defines as of kernel 5.0 and likely to + * be the case in the future. + * + * This is equivalent to libevdev_event_type_from_name_n() but takes the code + * name instead of the type name. + * + * @param name A non-NULL string describing an input-event value + * ("ABS_X", "REL_Y", "KEY_A", ...) + * @param len The length of the passed string excluding any terminating 0 + * character. + * + * @return The given event code for the name or -1 if not found. + */ +int +libevdev_event_type_from_code_name_n(const char *name, size_t len); + +/** + * @ingroup misc + * + * Look up an event code by its name. For example, the name "ABS_Y" + * returns 1. For the lookup to succeed, the name must be unique, which is + * the case for all defines as of kernel 5.0 and likely to be the case in + * the future. + * + * This is equivalent to libevdev_event_code_from_name() without the need + * for knowing the event type. + * + * @param name A non-NULL string describing an input-event value + * ("ABS_X", "REL_Y", "KEY_A", ...) + * + * @return The given event code for the name or -1 if not found. + */ +int +libevdev_event_code_from_code_name(const char *name); + +/** + * @ingroup misc + * + * Look up an event code by its name. For example, the name "ABS_Y" + * returns 1. For the lookup to succeed, the name must be unique, which is + * the case for all defines as of kernel 5.0 and likely to be the case in + * the future. + * + * This is equivalent to libevdev_event_code_from_name_n() without the need + * for knowing the event type. + * + * @param name A non-NULL string describing an input-event value + * ("ABS_X", "REL_Y", "KEY_A", ...) + * @param len The length of the passed string excluding any terminating 0 + * character. + * + * @return The given event code for the name or -1 if not found. + */ +int +libevdev_event_code_from_code_name_n(const char *name, size_t len); + +/** + * @ingroup misc + * + * Look up an event value by its type, code and name. Event values start + * with a fixed prefix followed by their name (eg., "MT_TOOL_PALM"). The + * prefix must be included in the name. It returns the constant assigned + * to the event code or -1 if not found. + * + * You have to pass the event type and code where to look for the name. For + * instance, to resolve "MT_TOOL_PALM" you need to pass EV_ABS as type, + * ABS_MT_TOOL_TYPE as code and "MT_TOOL_PALM" as string. + * + * As of kernel 4.17, only EV_ABS/ABS_MT_TOOL_TYPE support name resolution. + * + * @param type The event type (EV_* constant) where to look for the name. + * @param code The event code (ABS_* constant) where to look for the name. + * @param name A non-NULL string describing an input-event value + * ("MT_TOOL_TYPE", ...) + * @param len The length of the string in @p name excluding any terminating 0 + * character. + * + * @return The given value constant for the name or -1 if not found. + */ +int libevdev_event_value_from_name_n(unsigned int type, unsigned int code, + const char *name, size_t len); + +/** + * @ingroup misc + * + * Look up an input property by its name. Properties start with the fixed + * prefix "INPUT_PROP_" followed by their name (eg., "INPUT_PROP_POINTER"). + * The prefix must be included in the name. It returns the constant assigned + * to the property or -1 if not found. + * + * @param name A non-NULL string describing an input property + * + * @return The given code constant for the name or -1 if not found. + */ +int libevdev_property_from_name(const char *name); + +/** + * @ingroup misc + * + * Look up an input property by its name. Properties start with the fixed + * prefix "INPUT_PROP_" followed by their name (eg., "INPUT_PROP_POINTER"). + * The prefix must be included in the name. It returns the constant assigned + * to the property or -1 if not found. + * + * @param name A non-NULL string describing an input property + * @param len The length of the string in @p name excluding any terminating 0 + * character. + * + * @return The given code constant for the name or -1 if not found. + */ +int libevdev_property_from_name_n(const char *name, size_t len); + +/** + * @ingroup bits + * + * Get the repeat delay and repeat period values for this device. This + * function is a convenience function only, EV_REP is supported by + * libevdev_get_event_value(). + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param delay If not null, set to the repeat delay value + * @param period If not null, set to the repeat period value + * + * @return 0 on success, -1 if this device does not have repeat settings. + * + * @note This function is signal-safe + * + * @see libevdev_get_event_value + */ +int libevdev_get_repeat(const struct libevdev *dev, int *delay, int *period); + +/********* DEPRECATED SECTION *********/ +#if defined(__GNUC__) && __GNUC__ >= 4 +#define LIBEVDEV_DEPRECATED __attribute__ ((deprecated)) +#else +#define LIBEVDEV_DEPRECATED +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* LIBEVDEV_H */ diff --git a/nativelib/src/main/cpp/libevdev/libevdev.sym b/nativelib/src/main/cpp/libevdev/libevdev.sym new file mode 100644 index 0000000000..4161962dc8 --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/libevdev.sym @@ -0,0 +1,123 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright (c) 2013 David Herrmann + */ + +LIBEVDEV_1 { +global: + libevdev_change_fd; + libevdev_disable_event_code; + libevdev_disable_event_type; + libevdev_enable_event_code; + libevdev_enable_event_type; + libevdev_enable_property; + libevdev_event_code_from_name; + libevdev_event_code_from_name_n; + libevdev_event_code_get_name; + libevdev_event_is_code; + libevdev_event_is_type; + libevdev_event_type_from_name; + libevdev_event_type_from_name_n; + libevdev_event_type_get_max; + libevdev_event_type_get_name; + libevdev_fetch_event_value; + libevdev_fetch_slot_value; + libevdev_free; + libevdev_get_abs_flat; + libevdev_get_abs_fuzz; + libevdev_get_abs_info; + libevdev_get_abs_maximum; + libevdev_get_abs_minimum; + libevdev_get_abs_resolution; + libevdev_get_current_slot; + libevdev_get_driver_version; + libevdev_get_event_value; + libevdev_get_fd; + libevdev_get_id_bustype; + libevdev_get_id_product; + libevdev_get_id_vendor; + libevdev_get_id_version; + libevdev_get_log_priority; + libevdev_get_name; + libevdev_get_num_slots; + libevdev_get_phys; + libevdev_get_repeat; + libevdev_get_slot_value; + libevdev_get_uniq; + libevdev_grab; + libevdev_has_event_code; + libevdev_has_event_pending; + libevdev_has_event_type; + libevdev_has_property; + libevdev_kernel_set_abs_info; + libevdev_kernel_set_led_value; + libevdev_kernel_set_led_values; + libevdev_new; + libevdev_new_from_fd; + libevdev_next_event; + libevdev_property_get_name; + libevdev_set_abs_flat; + libevdev_set_abs_fuzz; + libevdev_set_abs_info; + libevdev_set_abs_maximum; + libevdev_set_abs_minimum; + libevdev_set_abs_resolution; + libevdev_set_clock_id; + libevdev_set_event_value; + libevdev_set_fd; + libevdev_set_id_bustype; + libevdev_set_id_product; + libevdev_set_id_vendor; + libevdev_set_id_version; + libevdev_set_log_function; + libevdev_set_log_priority; + libevdev_set_name; + libevdev_set_phys; + libevdev_set_slot_value; + libevdev_set_uniq; + libevdev_uinput_create_from_device; + libevdev_uinput_destroy; + libevdev_uinput_get_devnode; + libevdev_uinput_get_fd; + libevdev_uinput_get_syspath; + libevdev_uinput_write_event; + +local: + *; +}; + +LIBEVDEV_1_3 { +global: + libevdev_set_device_log_function; + libevdev_property_from_name; + libevdev_property_from_name_n; + +local: + *; +} LIBEVDEV_1; + +LIBEVDEV_1_6 { +global: + libevdev_event_value_get_name; + libevdev_event_value_from_name; + libevdev_event_value_from_name_n; +local: + *; +} LIBEVDEV_1_3; + +LIBEVDEV_1_7 { +global: + libevdev_event_code_from_code_name; + libevdev_event_code_from_code_name_n; + libevdev_event_type_from_code_name; + libevdev_event_type_from_code_name_n; +local: + *; +} LIBEVDEV_1_6; + +LIBEVDEV_1_10 { +global: + libevdev_disable_property; +local: + *; +} LIBEVDEV_1_7; diff --git a/nativelib/src/main/cpp/libevdev/make-event-names.py b/nativelib/src/main/cpp/libevdev/make-event-names.py new file mode 100755 index 0000000000..743b4b58b1 --- /dev/null +++ b/nativelib/src/main/cpp/libevdev/make-event-names.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# +# Parses linux/input.h scanning for #define KEY_FOO 134 +# Prints C header files or Python files that can be used as +# mapping and lookup tables. +# + +import re +import sys + + +class Bits(object): + def __init__(self): + self.max_codes = {} + + +prefixes = [ + "EV_", + "REL_", + "ABS_", + "KEY_", + "BTN_", + "LED_", + "SND_", + "MSC_", + "SW_", + "FF_", + "SYN_", + "REP_", + "INPUT_PROP_", + "MT_TOOL_", +] + +duplicates = [ + "EV_VERSION", + "BTN_MISC", + "BTN_MOUSE", + "BTN_JOYSTICK", + "BTN_GAMEPAD", + "BTN_DIGI", + "BTN_WHEEL", + "BTN_TRIGGER_HAPPY", + "SW_MAX", + "REP_MAX", +] + +btn_additional = [ + [0, "BTN_A"], + [0, "BTN_B"], + [0, "BTN_X"], + [0, "BTN_Y"], +] + +code_prefixes = [ + "REL_", + "ABS_", + "KEY_", + "BTN_", + "LED_", + "SND_", + "MSC_", + "SW_", + "FF_", + "SYN_", + "REP_", +] + + +def print_bits(bits, prefix): + if not hasattr(bits, prefix): + return + print("static const char * const %s_map[%s_MAX + 1] = {" % (prefix, prefix.upper())) + for val, name in sorted(list(getattr(bits, prefix).items())): + print(" [%s] = \"%s\"," % (name, name)) + if prefix == "key": + for val, name in sorted(list(getattr(bits, "btn").items())): + print(" [%s] = \"%s\"," % (name, name)) + print("};") + print("") + + +def print_map(bits): + print("static const char * const * const event_type_map[EV_MAX + 1] = {") + + for prefix in prefixes: + if prefix in ["BTN_", "EV_", "INPUT_PROP_", "MT_TOOL_"]: + continue + print(" [EV_%s] = %s_map," % (prefix[:-1], prefix[:-1].lower())) + + print("};") + print("") + + print("#if __clang__") + print("#pragma clang diagnostic push") + print("#pragma clang diagnostic ignored \"-Winitializer-overrides\"") + print("#elif __GNUC__") + print("#pragma GCC diagnostic push") + print("#pragma GCC diagnostic ignored \"-Woverride-init\"") + print("#endif") + print("static const int ev_max[EV_MAX + 1] = {") + for val in range(bits.max_codes["EV_MAX"] + 1): + if val in bits.ev: + prefix = bits.ev[val][3:] + if prefix + "_" in prefixes: + print(" %s_MAX," % prefix) + continue + print(" -1,") + print("};") + print("#if __clang__") + print("#pragma clang diagnostic pop /* \"-Winitializer-overrides\" */") + print("#elif __GNUC__") + print("#pragma GCC diagnostic pop /* \"-Woverride-init\" */") + print("#endif") + print("") + + +def print_lookup(bits, prefix): + if not hasattr(bits, prefix): + return + + names = sorted(list(getattr(bits, prefix).items())) + if prefix == "btn": + names = names + btn_additional + + # We need to manually add the _MAX codes because some are + # duplicates + maxname = "%s_MAX" % (prefix.upper()) + if maxname in duplicates: + names.append((bits.max_codes[maxname], maxname)) + + for val, name in sorted(names, key=lambda e: e[1]): + print(" { .name = \"%s\", .value = %s }," % (name, name)) + + +def print_lookup_table(bits): + print("struct name_entry {") + print(" const char *name;") + print(" unsigned int value;") + print("};") + print("") + print("static const struct name_entry tool_type_names[] = {") + print_lookup(bits, "mt_tool") + print("};") + print("") + print("static const struct name_entry ev_names[] = {") + print_lookup(bits, "ev") + print("};") + print("") + + print("static const struct name_entry code_names[] = {") + for prefix in sorted(code_prefixes, key=lambda e: e): + print_lookup(bits, prefix[:-1].lower()) + print("};") + print("") + print("static const struct name_entry prop_names[] = {") + print_lookup(bits, "input_prop") + print("};") + print("") + + +def print_mapping_table(bits): + print("/* THIS FILE IS GENERATED, DO NOT EDIT */") + print("") + print("#ifndef EVENT_NAMES_H") + print("#define EVENT_NAMES_H") + print("") + + for prefix in prefixes: + if prefix == "BTN_": + continue + print_bits(bits, prefix[:-1].lower()) + + print_map(bits) + print_lookup_table(bits) + + print("#endif /* EVENT_NAMES_H */") + + +def parse_define(bits, line): + m = re.match(r"^#define\s+(\w+)\s+(\w+)", line) + if m is None: + return + + name = m.group(1) + + try: + value = int(m.group(2), 0) + except ValueError: + return + + for prefix in prefixes: + if not name.startswith(prefix): + continue + + if name.endswith("_MAX"): + bits.max_codes[name] = value + + if name in duplicates: + return + + attrname = prefix[:-1].lower() + + if not hasattr(bits, attrname): + setattr(bits, attrname, {}) + b = getattr(bits, attrname) + b[value] = name + + +def parse(lines): + bits = Bits() + for line in lines: + if not line.startswith("#define"): + continue + parse_define(bits, line) + + return bits + + +def usage(prog): + print("Usage: %s ".format(prog)) + + +if __name__ == "__main__": + if len(sys.argv) <= 1: + usage(sys.argv[0]) + sys.exit(2) + + from itertools import chain + lines = chain(*[open(f).readlines() for f in sys.argv[1:]]) + bits = parse(lines) + print_mapping_table(bits) diff --git a/nativelib/src/main/cpp/logging.h b/nativelib/src/main/cpp/logging.h new file mode 100644 index 0000000000..91a750f4a0 --- /dev/null +++ b/nativelib/src/main/cpp/logging.h @@ -0,0 +1,30 @@ +#ifndef _LOGGING_H +#define _LOGGING_H + +#include +#include "android/log.h" + +#ifndef LOG_TAG +#define LOG_TAG "Key Mapper" +#endif + +#ifndef NO_LOG +#ifndef NO_DEBUG_LOG +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#else +#define LOGD(...) +#endif +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) +#else +#define LOGD(...) +#define LOGV(...) +#define LOGI(...) +#define LOGW(...) +#define LOGE(...) +#define PLOGE(fmt, args...) +#endif +#endif // _LOGGING_H diff --git a/nativelib/src/main/cpp/nativelib.cpp b/nativelib/src/main/cpp/nativelib.cpp new file mode 100644 index 0000000000..5553cad66f --- /dev/null +++ b/nativelib/src/main/cpp/nativelib.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include "libevdev/libevdev.h" + +#define LOG_TAG "KeyMapperNative" + +#include "logging.h" + +extern "C" +JNIEXPORT jstring JNICALL +Java_io_github_sds100_keymapper_nativelib_EvdevService_stringFromJNI(JNIEnv *env, + jobject /* this */) { + char *input_file_path = "/dev/input/event12"; + struct libevdev *dev = NULL; + int fd; + int rc = 1; + + fd = open(input_file_path, O_RDONLY); + + if (fd == -1) { + LOGE("Failed to open input file (%s)", + input_file_path); + return env->NewStringUTF("Failed"); + } + + rc = libevdev_new_from_fd(fd, &dev); + if (rc < 0) { + LOGE("Failed to init libevdev"); + return env->NewStringUTF("Failed to init"); + } + + __android_log_print(ANDROID_LOG_ERROR, "Key Mapper", "Input device name: \"%s\"\n", + libevdev_get_name(dev)); + __android_log_print(ANDROID_LOG_ERROR, "Key Mapper", + "Input device ID: bus %#x vendor %#x product %#x\n", + libevdev_get_id_bustype(dev), + libevdev_get_id_vendor(dev), + libevdev_get_id_product(dev)); + +// if (!libevdev_has_event_type(dev, EV_REL) || +// !libevdev_has_event_code(dev, EV_KEY, BTN_LEFT)) { +// printf("This device does not look like a mouse\n"); +// exit(1); +// } + libevdev_grab(dev, LIBEVDEV_GRAB); + + do { + struct input_event ev; + rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); + if (rc == 0) + __android_log_print(ANDROID_LOG_ERROR, "Key Mapper", "Event: %s %s %d\n", + libevdev_event_type_get_name(ev.type), + libevdev_event_code_get_name(ev.type, ev.code), + ev.value); + } while (rc == 1 || rc == 0 || rc == -EAGAIN); + + return env->NewStringUTF("Hello!"); +} \ No newline at end of file diff --git a/nativelib/src/main/java/io/github/sds100/keymapper/nativelib/EvdevService.kt b/nativelib/src/main/java/io/github/sds100/keymapper/nativelib/EvdevService.kt new file mode 100644 index 0000000000..1b76d10c55 --- /dev/null +++ b/nativelib/src/main/java/io/github/sds100/keymapper/nativelib/EvdevService.kt @@ -0,0 +1,45 @@ +package io.github.sds100.keymapper.nativelib + +import android.ddm.DdmHandleAppName +import android.system.Os +import android.util.Log +import kotlin.system.exitProcess + +/** + * See demo/service/UserService.java in the Shizuku-API repository for how a Shizuku user service + * is set up. + */ +class EvdevService : IEvdevService.Stub() { + + /** + * A native method that is implemented by the 'nativelib' native library, + * which is packaged with this application. + */ + external fun stringFromJNI(): String + + companion object { + private const val TAG: String = "EvdevService" + + @JvmStatic + fun main(args: Array) { + DdmHandleAppName.setAppName("keymapper_evdev", 0) + EvdevService() + } + } + + init { + Log.e(TAG, "SYSTEM PROPERTY ${System.getProperty("shizuku.library.path")}") + System.load("${System.getProperty("shizuku.library.path")}/libnativelib.so") + stringFromJNI() + } + + override fun destroy() { + Log.i(TAG, "destroy") + exitProcess(0) + } + + override fun sendEvent(): String { + Log.e(TAG, "UID = ${Os.getuid()}") + return stringFromJNI() + } +} diff --git a/privstarter/.gitignore b/privstarter/.gitignore new file mode 100644 index 0000000000..c591fdeb45 --- /dev/null +++ b/privstarter/.gitignore @@ -0,0 +1,2 @@ +/build +.cxx \ No newline at end of file diff --git a/privstarter/build.gradle.kts b/privstarter/build.gradle.kts new file mode 100644 index 0000000000..6814a832a1 --- /dev/null +++ b/privstarter/build.gradle.kts @@ -0,0 +1,78 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "io.github.sds100.keymapper.privstarter" + compileSdk = 35 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + + externalNativeBuild { + cmake { + // -DANDROID_STL=none is required by Rikka's library: https://github.com/RikkaW/libcxx-prefab + // -DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON is required to get the app running on the Android 15. This is related to the new 16kB page size support. + arguments("-DANDROID_STL=none", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON") + } + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + buildFeatures { + prefab = true + } + + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + + packaging { + jniLibs { + useLegacyPackaging = false + + // This is required on Android 15. Otherwise a java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH error is thrown. + keepDebugSymbols.add("**/*.so") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + implementation("org.conscrypt:conscrypt-android:2.5.3") + implementation("androidx.core:core-ktx:1.16.0") + implementation("androidx.appcompat:appcompat:1.7.0") + + // From Shizuku :manager module build.gradle file. + implementation("io.github.vvb2060.ndk:boringssl:20250114") + implementation("dev.rikka.ndk.thirdparty:cxx:1.2.0") + implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") + implementation("org.bouncycastle:bcpkix-jdk15on:1.70") + implementation("me.zhanghai.android.appiconloader:appiconloader:1.5.0") + implementation("dev.rikka.rikkax.core:core-ktx:1.4.1") +} \ No newline at end of file diff --git a/privstarter/consumer-rules.pro b/privstarter/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/privstarter/proguard-rules.pro b/privstarter/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/privstarter/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/privstarter/src/main/AndroidManifest.xml b/privstarter/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a5918e68ab --- /dev/null +++ b/privstarter/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/privstarter/src/main/cpp/CMakeLists.txt b/privstarter/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..f9d9670ce2 --- /dev/null +++ b/privstarter/src/main/cpp/CMakeLists.txt @@ -0,0 +1,86 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html. +# For more examples on how to use CMake, see https://github.com/android/ndk-samples. + +# Sets the minimum CMake version required for this project. +cmake_minimum_required(VERSION 3.22.1) + +# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, +# Since this is the top level CMakeLists.txt, the project name is also accessible +# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level +# build script scope). +project("privstarter") + +# FROM SHIZUKU +set(CMAKE_CXX_STANDARD 17) + +set(C_FLAGS "-Werror=format -fdata-sections -ffunction-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics") +set(LINKER_FLAGS "-Wl,--hash-style=both") + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + message("Builing Release...") + + set(C_FLAGS "${C_FLAGS} -O2 -fvisibility=hidden -fvisibility-inlines-hidden") + set(LINKER_FLAGS "${LINKER_FLAGS} -Wl,-exclude-libs,ALL -Wl,--gc-sections") +else() + message("Builing Debug...") + + add_definitions(-DDEBUG) +endif () + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${C_FLAGS}") + +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}") +set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}") + +find_library(log-lib log) +find_package(boringssl REQUIRED CONFIG) +find_package(cxx REQUIRED CONFIG) + +add_executable(libshizuku.so + starter.cpp misc.cpp selinux.cpp cgroup.cpp android.cpp) + +target_link_libraries(libshizuku.so ${log-lib} cxx::cxx) + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + add_custom_command(TARGET libshizuku.so POST_BUILD + COMMAND ${CMAKE_STRIP} --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libshizuku.so") +endif () + +add_library(adb SHARED + adb_pairing.cpp misc.cpp) + +target_link_libraries(adb ${log-lib} boringssl::crypto_static cxx::cxx) + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + add_custom_command(TARGET adb POST_BUILD + COMMAND ${CMAKE_STRIP} --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libadb.so") +endif () + +# END FROM SHIZUKU +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. +# +# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define +# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} +# is preferred for the same purpose. +# +# In order to load a library into your app from Java/Kotlin, you must call +# System.loadLibrary() and pass the name of the library defined here; +# for GameActivity/NativeActivity derived applications, the same library name must be +# used in the AndroidManifest.xml file. +add_library(${CMAKE_PROJECT_NAME} SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + privstarter.cpp) + +# Specifies libraries CMake should link to your target library. You +# can link libraries from various origins, such as libraries defined in this +# build script, prebuilt third-party libraries, or Android system libraries. +target_link_libraries(${CMAKE_PROJECT_NAME} + # List libraries link to the target library + android + log) + diff --git a/privstarter/src/main/cpp/adb_pairing.cpp b/privstarter/src/main/cpp/adb_pairing.cpp new file mode 100644 index 0000000000..1a44981e28 --- /dev/null +++ b/privstarter/src/main/cpp/adb_pairing.cpp @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "adb_pairing.h" + +#define LOG_TAG "AdbPairClient" + +#include "logging.h" + +// --------------------------------------------------------- + +static constexpr spake2_role_t kClientRole = spake2_role_alice; +static constexpr spake2_role_t kServerRole = spake2_role_bob; + +static const uint8_t kClientName[] = "adb pair client"; +static const uint8_t kServerName[] = "adb pair server"; + +static constexpr size_t kHkdfKeyLength = 16; + +struct PairingContextNative { + SPAKE2_CTX *spake2_ctx; + uint8_t key[SPAKE2_MAX_MSG_SIZE]; + size_t key_size; + + EVP_AEAD_CTX *aes_ctx; + uint64_t dec_sequence; + uint64_t enc_sequence; +}; + +static jlong PairingContext_Constructor(JNIEnv *env, jclass clazz, jboolean isClient, jbyteArray jPassword) { + spake2_role_t spake_role; + const uint8_t *my_name; + const uint8_t *their_name; + size_t my_len; + size_t their_len; + + if (isClient) { + spake_role = kClientRole; + my_name = kClientName; + my_len = sizeof(kClientName); + their_name = kServerName; + their_len = sizeof(kServerName); + } else { + spake_role = kServerRole; + my_name = kServerName; + my_len = sizeof(kServerName); + their_name = kClientName; + their_len = sizeof(kClientName); + } + + auto spake2_ctx = SPAKE2_CTX_new(spake_role, my_name, my_len, their_name, their_len); + if (spake2_ctx == nullptr) { + LOGE("Unable to create a SPAKE2 context."); + return 0; + } + + auto pswd_size = env->GetArrayLength(jPassword); + auto pswd = env->GetByteArrayElements(jPassword, nullptr); + + size_t key_size = 0; + uint8_t key[SPAKE2_MAX_MSG_SIZE]; + int status = SPAKE2_generate_msg(spake2_ctx, key, &key_size, SPAKE2_MAX_MSG_SIZE, (uint8_t *) pswd, pswd_size); + if (status != 1 || key_size == 0) { + LOGE("Unable to generate the SPAKE2 public key."); + + env->ReleaseByteArrayElements(jPassword, pswd, 0); + SPAKE2_CTX_free(spake2_ctx); + return 0; + } + env->ReleaseByteArrayElements(jPassword, pswd, 0); + + auto ctx = (PairingContextNative *) malloc(sizeof(PairingContextNative)); + memset(ctx, 0, sizeof(PairingContextNative)); + ctx->spake2_ctx = spake2_ctx; + memcpy(ctx->key, key, SPAKE2_MAX_MSG_SIZE); + ctx->key_size = key_size; + return (jlong) ctx; +} + +static jbyteArray PairingContext_Msg(JNIEnv *env, jobject obj, jlong ptr) { + auto ctx = (PairingContextNative *) ptr; + jbyteArray our_msg = env->NewByteArray(ctx->key_size); + env->SetByteArrayRegion(our_msg, 0, ctx->key_size, (jbyte *) ctx->key); + return our_msg; +} + +static jboolean PairingContext_InitCipher(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jTheirMsg) { + auto res = JNI_TRUE; + + auto ctx = (PairingContextNative *) ptr; + auto spake2_ctx = ctx->spake2_ctx; + auto their_msg_size = env->GetArrayLength(jTheirMsg); + + if (their_msg_size > SPAKE2_MAX_MSG_SIZE) { + LOGE("their_msg size [%d] greater then max size [%d].", their_msg_size, SPAKE2_MAX_MSG_SIZE); + return JNI_FALSE; + } + + auto their_msg = env->GetByteArrayElements(jTheirMsg, nullptr); + + size_t key_material_len = 0; + uint8_t key_material[SPAKE2_MAX_KEY_SIZE]; + int status = SPAKE2_process_msg(spake2_ctx, key_material, &key_material_len, + sizeof(key_material), (uint8_t *) their_msg, their_msg_size); + + env->ReleaseByteArrayElements(jTheirMsg, their_msg, 0); + + if (status != 1) { + LOGE("Unable to process their public key"); + return JNI_FALSE; + } + + // -------- + uint8_t key[kHkdfKeyLength]; + uint8_t info[] = "adb pairing_auth aes-128-gcm key"; + + status = HKDF(key, sizeof(key), EVP_sha256(), key_material, key_material_len, nullptr, 0, info, + sizeof(info) - 1); + if (status != 1) { + LOGE("HKDF"); + return JNI_FALSE; + } + + ctx->aes_ctx = EVP_AEAD_CTX_new(EVP_aead_aes_128_gcm(), key, sizeof(key), EVP_AEAD_DEFAULT_TAG_LENGTH); + + if (!ctx->aes_ctx) { + LOGE("EVP_AEAD_CTX_new"); + return JNI_FALSE; + } + + return res; +} + +static jbyteArray PairingContext_Encrypt(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jIn) { + auto ctx = (PairingContextNative *) ptr; + auto aes_ctx = ctx->aes_ctx; + + auto in = env->GetByteArrayElements(jIn, nullptr); + auto in_size = env->GetArrayLength(jIn); + + auto out_size = (size_t) in_size + EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(ctx->aes_ctx)); + uint8_t out[out_size]; + + auto nonce_size = EVP_AEAD_nonce_length(EVP_AEAD_CTX_aead(aes_ctx)); + uint8_t nonce[nonce_size]; + memset(nonce, 0, nonce_size); + memcpy(nonce, &ctx->enc_sequence, sizeof(ctx->enc_sequence)); + + size_t written_sz; + int status = EVP_AEAD_CTX_seal(aes_ctx, out, &written_sz, out_size, nonce, nonce_size, (uint8_t *) in, in_size, nullptr, 0); + + env->ReleaseByteArrayElements(jIn, in, 0); + + if (!status) { + LOGE("Failed to encrypt (in_len=%d, out_len=%" PRIuPTR", out_len_needed=%d)", in_size, out_size, in_size); + return nullptr; + } + ++ctx->enc_sequence; + + jbyteArray jOut = env->NewByteArray(written_sz); + env->SetByteArrayRegion(jOut, 0, written_sz, (jbyte *) out); + return jOut; +} + +static jbyteArray PairingContext_Decrypt(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jIn) { + auto ctx = (PairingContextNative *) ptr; + auto aes_ctx = ctx->aes_ctx; + + auto in = env->GetByteArrayElements(jIn, nullptr); + auto in_size = env->GetArrayLength(jIn); + + auto out_size = (size_t) in_size; + uint8_t out[out_size]; + + auto nonce_size = EVP_AEAD_nonce_length(EVP_AEAD_CTX_aead(aes_ctx)); + uint8_t nonce[nonce_size]; + memset(nonce, 0, nonce_size); + memcpy(nonce, &ctx->dec_sequence, sizeof(ctx->dec_sequence)); + + size_t written_sz; + int status = EVP_AEAD_CTX_open(aes_ctx, out, &written_sz, out_size, nonce, nonce_size, (uint8_t *) in, in_size, nullptr, 0); + + env->ReleaseByteArrayElements(jIn, in, 0); + + if (!status) { + LOGE("Failed to decrypt (in_len=%d, out_len=%" PRIuPTR", out_len_needed=%d)", in_size, out_size, in_size); + return nullptr; + } + ++ctx->dec_sequence; + + jbyteArray jOut = env->NewByteArray(written_sz); + env->SetByteArrayRegion(jOut, 0, written_sz, (jbyte *) out); + return jOut; +} + +static void PairingContext_Destroy(JNIEnv *env, jobject obj, jlong ptr) { + auto ctx = (PairingContextNative *) ptr; + SPAKE2_CTX_free(ctx->spake2_ctx); + if (ctx->aes_ctx) EVP_AEAD_CTX_free(ctx->aes_ctx); + free(ctx); +} + +// --------------------------------------------------------- + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + JNIEnv *env = nullptr; + + if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) + return -1; + + JNINativeMethod methods_PairingContext[] = { + {"nativeConstructor", "(Z[B)J", (void *) PairingContext_Constructor}, + {"nativeMsg", "(J)[B", (void *) PairingContext_Msg}, + {"nativeInitCipher", "(J[B)Z", (void *) PairingContext_InitCipher}, + {"nativeEncrypt", "(J[B)[B", (void *) PairingContext_Encrypt}, + {"nativeDecrypt", "(J[B)[B", (void *) PairingContext_Decrypt}, + {"nativeDestroy", "(J)V", (void *) PairingContext_Destroy}, + }; + + env->RegisterNatives(env->FindClass("moe/shizuku/manager/adb/PairingContext"), methods_PairingContext, + sizeof(methods_PairingContext) / sizeof(JNINativeMethod)); + + return JNI_VERSION_1_6; +} diff --git a/privstarter/src/main/cpp/adb_pairing.h b/privstarter/src/main/cpp/adb_pairing.h new file mode 100644 index 0000000000..3f9eb70a66 --- /dev/null +++ b/privstarter/src/main/cpp/adb_pairing.h @@ -0,0 +1,4 @@ +#ifndef ADB_H +#define ADB_H + +#endif // ADB_H diff --git a/privstarter/src/main/cpp/android.cpp b/privstarter/src/main/cpp/android.cpp new file mode 100644 index 0000000000..4bb6c44e0e --- /dev/null +++ b/privstarter/src/main/cpp/android.cpp @@ -0,0 +1,30 @@ +#include +#include +#include +#include +#include + +namespace android { + + int GetApiLevel() { + static int apiLevel = 0; + if (apiLevel > 0) return apiLevel; + + char buf[PROP_VALUE_MAX + 1]; + if (__system_property_get("ro.build.version.sdk", buf) > 0) + apiLevel = atoi(buf); + + return apiLevel; + } + + int GetPreviewApiLevel() { + static int previewApiLevel = 0; + if (previewApiLevel > 0) return previewApiLevel; + + char buf[PROP_VALUE_MAX + 1]; + if (__system_property_get("ro.build.version.preview_sdk", buf) > 0) + previewApiLevel = atoi(buf); + + return previewApiLevel; + } +} \ No newline at end of file diff --git a/privstarter/src/main/cpp/android.h b/privstarter/src/main/cpp/android.h new file mode 100644 index 0000000000..a4b78fba96 --- /dev/null +++ b/privstarter/src/main/cpp/android.h @@ -0,0 +1,8 @@ +#pragma once + +namespace android { + + int GetApiLevel(); + + int GetPreviewApiLevel(); +} \ No newline at end of file diff --git a/privstarter/src/main/cpp/cgroup.cpp b/privstarter/src/main/cpp/cgroup.cpp new file mode 100644 index 0000000000..c1015b72f5 --- /dev/null +++ b/privstarter/src/main/cpp/cgroup.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include + +namespace cgroup { + + static ssize_t fdgets(char *buf, const size_t size, int fd) { + ssize_t len = 0; + buf[0] = '\0'; + while (len < size - 1) { + ssize_t ret = read(fd, buf + len, 1); + if (ret < 0) + return -1; + if (ret == 0) + break; + if (buf[len] == '\0' || buf[len++] == '\n') { + break; + } + } + buf[len] = '\0'; + buf[size - 1] = '\0'; + return len; + } + + int get_cgroup(int pid, int* cuid, int *cpid) { + char buf[PATH_MAX]; + snprintf(buf, PATH_MAX, "/proc/%d/cgroup", pid); + + int fd = open(buf, O_RDONLY); + if (fd == -1) + return -1; + + while (fdgets(buf, PATH_MAX, fd) > 0) { + if (sscanf(buf, "%*d:cpuacct:/uid_%d/pid_%d", cuid, cpid) == 2) { + close(fd); + return 0; + } + } + close(fd); + return -1; + } + + static int switch_cgroup(int pid, int cuid, int cpid, const char *name) { + char buf[PATH_MAX]; + if (cuid != -1 && cpid != -1) { + snprintf(buf, PATH_MAX, "/acct/uid_%d/pid_%d/%s", cuid, cpid, name); + } else { + snprintf(buf, PATH_MAX, "/acct/%s", name); + } + + int fd = open(buf, O_WRONLY | O_APPEND); + if (fd == -1) + return -1; + + snprintf(buf, PATH_MAX, "%d\n", pid); + if (write(fd, buf, strlen(buf)) == -1) { + close(fd); + return -1; + } + + close(fd); + return 0; + } + + int switch_cgroup(int pid, int cuid, int cpid) { + int res = 0; + res += switch_cgroup(pid, cuid, cpid, "cgroup.procs"); + res += switch_cgroup(pid, cuid, cpid, "tasks"); + return res; + } + +} \ No newline at end of file diff --git a/privstarter/src/main/cpp/cgroup.h b/privstarter/src/main/cpp/cgroup.h new file mode 100644 index 0000000000..361a0b1919 --- /dev/null +++ b/privstarter/src/main/cpp/cgroup.h @@ -0,0 +1,9 @@ +#ifndef CGROUP_H +#define CGROUP_H + +namespace cgroup { + int get_cgroup(int pid, int* cuid, int *cpid); + int switch_cgroup(int pid, int cuid, int cpid); +} + +#endif // CGROUP_H diff --git a/privstarter/src/main/cpp/helper.cpp b/privstarter/src/main/cpp/helper.cpp new file mode 100644 index 0000000000..1001f8316d --- /dev/null +++ b/privstarter/src/main/cpp/helper.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "selinux.h" + +#define LOG_TAG "ShizukuServer" + +#include "logging.h" + +static jint setcontext(JNIEnv *env, jobject thiz, jstring jName) { + const char *name = env->GetStringUTFChars(jName, nullptr); + + if (!se::setcon) + return -1; + + int res = se::setcon(name); + if (res == -1) PLOGE("setcon %s", name); + + env->ReleaseStringUTFChars(jName, name); + + return res; +} + +static JNINativeMethod gMethods[] = { + {"setSELinuxContext", "(Ljava/lang/String;)I", (void *) setcontext}, +}; + +static int registerNativeMethods(JNIEnv *env, const char *className, + JNINativeMethod *gMethods, int numMethods) { + jclass clazz; + clazz = env->FindClass(className); + if (clazz == nullptr) + return JNI_FALSE; + + if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) + return JNI_FALSE; + + return JNI_TRUE; +} + +static int registerNatives(JNIEnv *env) { + if (!registerNativeMethods(env, "moe/shizuku/server/utils/NativeHelper", gMethods, + sizeof(gMethods) / sizeof(gMethods[0]))) + return JNI_FALSE; + + return JNI_TRUE; +} + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + JNIEnv *env = nullptr; + jint result; + + if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) + return -1; + + assert(env != nullptr); + + se::init(); + + if (!registerNatives(env)) { + LOGE("registerNatives NativeHelper"); + return -1; + } + + result = JNI_VERSION_1_6; + + return result; +} diff --git a/privstarter/src/main/cpp/logging.h b/privstarter/src/main/cpp/logging.h new file mode 100644 index 0000000000..91a750f4a0 --- /dev/null +++ b/privstarter/src/main/cpp/logging.h @@ -0,0 +1,30 @@ +#ifndef _LOGGING_H +#define _LOGGING_H + +#include +#include "android/log.h" + +#ifndef LOG_TAG +#define LOG_TAG "Key Mapper" +#endif + +#ifndef NO_LOG +#ifndef NO_DEBUG_LOG +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#else +#define LOGD(...) +#endif +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) +#else +#define LOGD(...) +#define LOGV(...) +#define LOGI(...) +#define LOGW(...) +#define LOGE(...) +#define PLOGE(fmt, args...) +#endif +#endif // _LOGGING_H diff --git a/privstarter/src/main/cpp/misc.cpp b/privstarter/src/main/cpp/misc.cpp new file mode 100644 index 0000000000..e7d2f8ccd7 --- /dev/null +++ b/privstarter/src/main/cpp/misc.cpp @@ -0,0 +1,195 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "misc.h" + +ssize_t fdgets(char *buf, const size_t size, int fd) { + ssize_t len = 0; + buf[0] = '\0'; + while (len < size - 1) { + ssize_t ret = read(fd, buf + len, 1); + if (ret < 0) + return -1; + if (ret == 0) + break; + if (buf[len] == '\0' || buf[len++] == '\n') { + buf[len] = '\0'; + break; + } + } + buf[len] = '\0'; + buf[size - 1] = '\0'; + return len; +} + +int get_proc_name(int pid, char *name, size_t _size) { + int fd; + ssize_t __size; + + char buf[1024]; + snprintf(buf, sizeof(buf), "/proc/%d/cmdline", pid); + if (access(buf, R_OK) == -1 || (fd = open(buf, O_RDONLY)) == -1) + return 1; + if ((__size = fdgets(buf, sizeof(buf), fd)) == 0) { + snprintf(buf, sizeof(buf), "/proc/%d/comm", pid); + close(fd); + if (access(buf, R_OK) == -1 || (fd = open(buf, O_RDONLY)) == -1) + return 1; + __size = fdgets(buf, sizeof(buf), fd); + } + close(fd); + + if (__size < _size) { + strncpy(name, buf, static_cast(__size)); + name[__size] = '\0'; + } else { + strncpy(name, buf, _size); + name[_size] = '\0'; + } + + return 0; +} + +int is_num(const char *s) { + size_t len = strlen(s); + for (size_t i = 0; i < len; ++i) + if (s[i] < '0' || s[i] > '9') + return 0; + return 1; +} + +int copyfileat(int src_path_fd, const char *src_path, int dst_path_fd, const char *dst_path) { + int src_fd; + int dst_fd; + struct stat stat_buf{}; + int64_t size_remaining; + size_t count; + ssize_t result; + + if ((src_fd = openat(src_path_fd, src_path, O_RDONLY)) == -1) + return -1; + + if (fstat(src_fd, &stat_buf) == -1) + return -1; + + dst_fd = openat(dst_path_fd, dst_path, O_WRONLY | O_CREAT | O_TRUNC, stat_buf.st_mode); + if (dst_fd == -1) { + close(src_fd); + return -1; + } + + size_remaining = stat_buf.st_size; + for (;;) { + if (size_remaining > 0x7ffff000) + count = 0x7ffff000; + else + count = static_cast(size_remaining); + + result = sendfile(dst_fd, src_fd, nullptr, count); + if (result == -1) { + close(src_fd); + close(dst_fd); + unlink(dst_path); + return -1; + } + + size_remaining -= result; + if (size_remaining == 0) { + close(src_fd); + close(dst_fd); + return 0; + } + } +} + +int copyfile(const char *src_path, const char *dst_path) { + return copyfileat(0, src_path, 0, dst_path); +} + +uintptr_t memsearch(const uintptr_t start, const uintptr_t end, const void *value, size_t size) { + uintptr_t _start = start; + while (true) { + if (_start + size >= end) + return 0; + + if (memcmp((const void *) _start, value, size) == 0) + return _start; + + _start += 1; + } +} + +int switch_mnt_ns(int pid) { + char mnt[32]; + snprintf(mnt, sizeof(mnt), "/proc/%d/ns/mnt", pid); + if (access(mnt, R_OK) == -1) return -1; + + int fd = open(mnt, O_RDONLY); + if (fd < 0) return -1; + + int res = setns(fd, 0); + close(fd); + return res; +} + +void foreach_proc(foreach_proc_function *func) { + DIR *dir; + struct dirent *entry; + + if (!(dir = opendir("/proc"))) + return; + + while ((entry = readdir(dir))) { + if (entry->d_type != DT_DIR) continue; + if (!is_num(entry->d_name)) continue; + pid_t pid = atoi(entry->d_name); + func(pid); + } + + closedir(dir); +} + +char *trim(char *str) { + size_t len = 0; + char *frontp = str; + char *endp = nullptr; + + if (str == nullptr) { return nullptr; } + if (str[0] == '\0') { return str; } + + len = strlen(str); + endp = str + len; + + /* Move the front and back pointers to address the first non-whitespace + * characters from each end. + */ + while (isspace((unsigned char) *frontp)) { ++frontp; } + if (endp != frontp) { + while (isspace((unsigned char) *(--endp)) && endp != frontp) {} + } + + if (str + len - 1 != endp) + *(endp + 1) = '\0'; + else if (frontp != str && endp == frontp) + *str = '\0'; + + /* Shift the string so that it starts at str so that if it's dynamically + * allocated, we can still free it on the returned pointer. Note the reuse + * of endp to mean the front of the string buffer now. + */ + endp = str; + if (frontp != str) { + while (*frontp) { *endp++ = *frontp++; } + *endp = '\0'; + } + + return str; +} \ No newline at end of file diff --git a/privstarter/src/main/cpp/misc.h b/privstarter/src/main/cpp/misc.h new file mode 100644 index 0000000000..3ab3e87d4a --- /dev/null +++ b/privstarter/src/main/cpp/misc.h @@ -0,0 +1,14 @@ +#ifndef MISC_H +#define MISC_H + +int copyfile(const char *src_path, const char *dst_path); +uintptr_t memsearch(const uintptr_t start, const uintptr_t end, const void *value, size_t size); +int switch_mnt_ns(int pid); +int get_proc_name(int pid, char *name, size_t _size); + +using foreach_proc_function = void(pid_t); +void foreach_proc(foreach_proc_function *func); + +char *trim(char *str); + +#endif // MISC_H diff --git a/privstarter/src/main/cpp/privstarter.cpp b/privstarter/src/main/cpp/privstarter.cpp new file mode 100644 index 0000000000..303a24597b --- /dev/null +++ b/privstarter/src/main/cpp/privstarter.cpp @@ -0,0 +1,9 @@ +#include + +extern "C" JNIEXPORT jstring JNICALL +Java_io_github_sds100_keymapper_privstarter_NativeLib_stringFromJNI( + JNIEnv* env, + jobject /* this */) { + char* hello = "Hello from C++"; + return env->NewStringUTF(hello); +} \ No newline at end of file diff --git a/privstarter/src/main/cpp/selinux.cpp b/privstarter/src/main/cpp/selinux.cpp new file mode 100644 index 0000000000..9b9ebc63cf --- /dev/null +++ b/privstarter/src/main/cpp/selinux.cpp @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include +#include +#include "selinux.h" + +namespace se { + + static int __getcon(char **context) { + int fd = open("/proc/self/attr/current", O_RDONLY | O_CLOEXEC); + if (fd < 0) + return fd; + + char *buf; + size_t size; + int errno_hold; + ssize_t ret; + + size = sysconf(_SC_PAGE_SIZE); + buf = (char *) malloc(size); + if (!buf) { + ret = -1; + goto out; + } + memset(buf, 0, size); + + do { + ret = read(fd, buf, size - 1); + } while (ret < 0 && errno == EINTR); + if (ret < 0) + goto out2; + + if (ret == 0) { + *context = nullptr; + goto out2; + } + + *context = strdup(buf); + if (!(*context)) { + ret = -1; + goto out2; + } + ret = 0; + out2: + free(buf); + out: + errno_hold = errno; + close(fd); + errno = errno_hold; + return 0; + } + + static int __setcon(const char *ctx) { + int fd = open("/proc/self/attr/current", O_WRONLY | O_CLOEXEC); + if (fd < 0) + return fd; + size_t len = strlen(ctx) + 1; + ssize_t rc = write(fd, ctx, len); + close(fd); + return rc != len; + } + + static int __setfilecon(const char *path, const char *ctx) { + int rc = syscall(__NR_setxattr, path, "security.selinux"/*XATTR_NAME_SELINUX*/, ctx, + strlen(ctx) + 1, 0); + if (rc) { + errno = -rc; + return -1; + } + return 0; + } + + static int __selinux_check_access(const char *scon, const char *tcon, + const char *tclass, const char *perm, void *auditdata) { + return 0; + } + + static void __freecon(char *con) { + free(con); + } + + getcon_t *getcon = __getcon; + setcon_t *setcon = __setcon; + setfilecon_t *setfilecon = __setfilecon; + selinux_check_access_t *selinux_check_access = __selinux_check_access; + freecon_t *freecon = __freecon; + + void init() { + if (access("/system/lib/libselinux.so", F_OK) != 0 && access("/system/lib64/libselinux.so", F_OK) != 0) + return; + + void *handle = dlopen("libselinux.so", RTLD_LAZY | RTLD_LOCAL); + if (handle == nullptr) + return; + + getcon = (getcon_t *) dlsym(handle, "getcon"); + setcon = (setcon_t *) dlsym(handle, "setcon"); + setfilecon = (setfilecon_t *) dlsym(handle, "setfilecon"); + selinux_check_access = (selinux_check_access_t *) dlsym(handle, "selinux_check_access"); + freecon = (freecon_t *) (dlsym(handle, "freecon")); + } +} diff --git a/privstarter/src/main/cpp/selinux.h b/privstarter/src/main/cpp/selinux.h new file mode 100644 index 0000000000..f5e47eb291 --- /dev/null +++ b/privstarter/src/main/cpp/selinux.h @@ -0,0 +1,21 @@ +#ifndef SELINUX_H +#define SELINUX_H + +namespace se { + void init(); + + using getcon_t = int(char **); + using setcon_t = int(const char *); + using setfilecon_t = int(const char *, const char *); + using selinux_check_access_t = int(const char *, const char *, const char *, const char *, + void *); + using freecon_t = void(char *); + + extern getcon_t *getcon; + extern setcon_t *setcon; + extern setfilecon_t *setfilecon; + extern selinux_check_access_t *selinux_check_access; + extern freecon_t *freecon; +} + +#endif // SELINUX_H diff --git a/privstarter/src/main/cpp/starter.cpp b/privstarter/src/main/cpp/starter.cpp new file mode 100644 index 0000000000..372c154e79 --- /dev/null +++ b/privstarter/src/main/cpp/starter.cpp @@ -0,0 +1,326 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "android.h" +#include "misc.h" +#include "selinux.h" +#include "cgroup.h" +#include "logging.h" + +#ifdef DEBUG +#define JAVA_DEBUGGABLE +#endif + +#define perrorf(...) fprintf(stderr, __VA_ARGS__) + +#define EXIT_FATAL_SET_CLASSPATH 3 +#define EXIT_FATAL_FORK 4 +#define EXIT_FATAL_APP_PROCESS 5 +#define EXIT_FATAL_UID 6 +#define EXIT_FATAL_PM_PATH 7 +#define EXIT_FATAL_KILL 9 +#define EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX 10 + +#define PACKAGE_NAME "io.github.sds100.keymapper.debug" +#define SERVER_NAME "shizuku_server" +#define SERVER_CLASS_PATH "io.github.sds100.keymapper.nativelib.EvdevService" + +#if defined(__arm__) +#define ABI "armeabi-v7a" +#elif defined(__i386__) +#define ABI "x86" +#elif defined(__x86_64__) +#define ABI "x86_64" +#elif defined(__aarch64__) +#define ABI "arm64-v8a" +#endif + +static void run_server(const char *apk_path, const char *lib_path, const char *main_class, + const char *process_name) { + if (setenv("CLASSPATH", apk_path, true)) { + LOGE("can't set CLASSPATH\n"); + exit(EXIT_FATAL_SET_CLASSPATH); + } + +#define ARG(v) char **v = nullptr; \ + char buf_##v[PATH_MAX]; \ + size_t v_size = 0; \ + uintptr_t v_current = 0; +#define ARG_PUSH(v, arg) v_size += sizeof(char *); \ +if (v == nullptr) { \ + v = (char **) malloc(v_size); \ +} else { \ + v = (char **) realloc(v, v_size);\ +} \ +v_current = (uintptr_t) v + v_size - sizeof(char *); \ +*((char **) v_current) = arg ? strdup(arg) : nullptr; + +#define ARG_END(v) ARG_PUSH(v, nullptr) + +#define ARG_PUSH_FMT(v, fmt, ...) snprintf(buf_##v, PATH_MAX, fmt, __VA_ARGS__); \ + ARG_PUSH(v, buf_##v) + +#ifdef JAVA_DEBUGGABLE +#define ARG_PUSH_DEBUG_ONLY(v, arg) ARG_PUSH(v, arg) +#define ARG_PUSH_DEBUG_VM_PARAMS(v) \ + if (android::GetApiLevel() >= 30) { \ + ARG_PUSH(v, "-Xcompiler-option"); \ + ARG_PUSH(v, "--debuggable"); \ + ARG_PUSH(v, "-XjdwpProvider:adbconnection"); \ + ARG_PUSH(v, "-XjdwpOptions:suspend=n,server=y"); \ + } else if (android::GetApiLevel() >= 28) { \ + ARG_PUSH(v, "-Xcompiler-option"); \ + ARG_PUSH(v, "--debuggable"); \ + ARG_PUSH(v, "-XjdwpProvider:internal"); \ + ARG_PUSH(v, "-XjdwpOptions:transport=dt_android_adb,suspend=n,server=y"); \ + } else { \ + ARG_PUSH(v, "-Xcompiler-option"); \ + ARG_PUSH(v, "--debuggable"); \ + ARG_PUSH(v, "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"); \ + } +#else +#define ARG_PUSH_DEBUG_VM_PARAMS(v) +#define ARG_PUSH_DEBUG_ONLY(v, arg) +#endif + + ARG(argv) + ARG_PUSH(argv, "/system/bin/app_process") + ARG_PUSH_FMT(argv, "-Djava.class.path=%s", apk_path) + ARG_PUSH_FMT(argv, "-Dshizuku.library.path=%s", lib_path) + ARG_PUSH_DEBUG_VM_PARAMS(argv) + ARG_PUSH(argv, "/system/bin") + ARG_PUSH_FMT(argv, "--nice-name=%s", process_name) + ARG_PUSH(argv, main_class) + ARG_PUSH_DEBUG_ONLY(argv, "--debug") + ARG_END(argv) + + LOGD("exec app_process"); + + if (execvp((const char *) argv[0], argv)) { + exit(EXIT_FATAL_APP_PROCESS); + } +} + +static void start_server(const char *apk_path, const char *lib_path, const char *main_class, + const char *process_name) { + + if (daemon(false, false) == 0) { + LOGD("child"); + run_server(apk_path, lib_path, main_class, process_name); + } else { + perrorf("fatal: can't fork\n"); + exit(EXIT_FATAL_FORK); + } +} + +static int check_selinux(const char *s, const char *t, const char *c, const char *p) { + int res = se::selinux_check_access(s, t, c, p, nullptr); +#ifndef DEBUG + if (res != 0) { +#endif + printf("info: selinux_check_access %s %s %s %s: %d\n", s, t, c, p, res); + fflush(stdout); +#ifndef DEBUG + } +#endif + return res; +} + +static int switch_cgroup() { + int s_cuid, s_cpid; + int spid = getpid(); + + if (cgroup::get_cgroup(spid, &s_cuid, &s_cpid) != 0) { + printf("warn: can't read cgroup\n"); + fflush(stdout); + return -1; + } + + printf("info: cgroup is /uid_%d/pid_%d\n", s_cuid, s_cpid); + fflush(stdout); + + if (cgroup::switch_cgroup(spid, -1, -1) != 0) { + printf("warn: can't switch cgroup\n"); + fflush(stdout); + return -1; + } + + if (cgroup::get_cgroup(spid, &s_cuid, &s_cpid) != 0) { + printf("info: switch cgroup succeeded\n"); + fflush(stdout); + return 0; + } + + printf("warn: can't switch self, current cgroup is /uid_%d/pid_%d\n", s_cuid, s_cpid); + fflush(stdout); + return -1; +} + +char *context = nullptr; + +int starter_main(int argc, char *argv[]) { + char *apk_path = nullptr; + char *lib_path = nullptr; + + // Get the apk path from the program arguments. This gets the path by setting the + // start of the apk path array to after the "--apk=" by offsetting by 6 characters. + for (int i = 0; i < argc; ++i) { + if (strncmp(argv[i], "--apk=", 6) == 0) { + apk_path = argv[i] + 6; + } else if (strncmp(argv[i], "--lib=", 6) == 0) { + lib_path = argv[i] + 6; + } + } + + printf("info: apk path = %s\n", apk_path); + + int uid = getuid(); + if (uid != 0 && uid != 2000) { + perrorf("fatal: run Shizuku from non root nor adb user (uid=%d).\n", uid); + exit(EXIT_FATAL_UID); + } + + se::init(); + + if (uid == 0) { + chown("/data/local/tmp/shizuku_starter", 2000, 2000); + se::setfilecon("/data/local/tmp/shizuku_starter", "u:object_r:shell_data_file:s0"); + switch_cgroup(); + + int sdkLevel = 0; + char buf[PROP_VALUE_MAX + 1]; + if (__system_property_get("ro.build.version.sdk", buf) > 0) + sdkLevel = atoi(buf); + + if (sdkLevel >= 29) { + printf("info: switching mount namespace to init...\n"); + switch_mnt_ns(1); + } + } + + if (uid == 0) { + if (se::getcon(&context) == 0) { + int res = 0; + + res |= check_selinux("u:r:untrusted_app:s0", context, "binder", "call"); + res |= check_selinux("u:r:untrusted_app:s0", context, "binder", "transfer"); + + if (res != 0) { + perrorf("fatal: the su you are using does not allow app (u:r:untrusted_app:s0) to connect to su (%s) with binder.\n", + context); + exit(EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX); + } + se::freecon(context); + } + } + + mkdir("/data/local/tmp/shizuku", 0707); + chmod("/data/local/tmp/shizuku", 0707); + if (uid == 0) { + chown("/data/local/tmp/shizuku", 2000, 2000); + se::setfilecon("/data/local/tmp/shizuku", "u:object_r:shell_data_file:s0"); + } + + printf("info: starter begin\n"); + fflush(stdout); + + // kill old server + printf("info: killing old process...\n"); + fflush(stdout); + + foreach_proc([](pid_t pid) { + if (pid == getpid()) return; + + char name[1024]; + if (get_proc_name(pid, name, 1024) != 0) return; + + if (strcmp(SERVER_NAME, name) != 0 + && strcmp("shizuku_server_legacy", name) != 0) + return; + + if (kill(pid, SIGKILL) == 0) + printf("info: killed %d (%s)\n", pid, name); + else if (errno == EPERM) { + perrorf("fatal: can't kill %d, please try to stop existing Shizuku from app first.\n", + pid); + exit(EXIT_FATAL_KILL); + } else { + printf("warn: failed to kill %d (%s)\n", pid, name); + } + }); + + if (access(apk_path, R_OK) == 0) { + printf("info: use apk path from argv\n"); + fflush(stdout); + } + + if (access(lib_path, R_OK) == 0) { + printf("info: use lib path from argv\n"); + fflush(stdout); + } + + if (!apk_path) { + auto f = popen("pm path " PACKAGE_NAME, "r"); + if (f) { + char line[PATH_MAX]{0}; + fgets(line, PATH_MAX, f); + trim(line); + if (strstr(line, "package:") == line) { + apk_path = line + strlen("package:"); + } + pclose(f); + } + } + + if (!apk_path) { + perrorf("fatal: can't get path of manager\n"); + exit(EXIT_FATAL_PM_PATH); + } + + if (!lib_path) { + perrorf("fatal: can't get path of native libraries\n"); + exit(EXIT_FATAL_PM_PATH); + } + + printf("info: apk path is %s\n", apk_path); + printf("info: lib path is %s\n", lib_path); + if (access(apk_path, R_OK) != 0) { + perrorf("fatal: can't access manager %s\n", apk_path); + exit(EXIT_FATAL_PM_PATH); + } + + printf("info: starting server...\n"); + fflush(stdout); + LOGD("start_server"); + start_server(apk_path, lib_path, SERVER_CLASS_PATH, SERVER_NAME); + exit(EXIT_SUCCESS); +} + +using main_func = int (*)(int, char *[]); + +static main_func applet_main[] = {starter_main, nullptr}; + +int main(int argc, char **argv) { + std::string_view base = basename(argv[0]); + + LOGD("applet %s", base.data()); + + constexpr const char *applet_names[] = {"shizuku_starter", nullptr}; + + for (int i = 0; applet_names[i]; ++i) { + if (base == applet_names[i]) { + return (*applet_main[i])(argc, argv); + } + } + + return 1; +} diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbClient.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbClient.kt new file mode 100644 index 0000000000..f2f822e80a --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbClient.kt @@ -0,0 +1,180 @@ +package moe.shizuku.manager.adb + +import android.os.Build +import android.util.Log +import moe.shizuku.manager.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY +import moe.shizuku.manager.adb.AdbProtocol.ADB_AUTH_SIGNATURE +import moe.shizuku.manager.adb.AdbProtocol.ADB_AUTH_TOKEN +import moe.shizuku.manager.adb.AdbProtocol.A_AUTH +import moe.shizuku.manager.adb.AdbProtocol.A_CLSE +import moe.shizuku.manager.adb.AdbProtocol.A_CNXN +import moe.shizuku.manager.adb.AdbProtocol.A_MAXDATA +import moe.shizuku.manager.adb.AdbProtocol.A_OKAY +import moe.shizuku.manager.adb.AdbProtocol.A_OPEN +import moe.shizuku.manager.adb.AdbProtocol.A_STLS +import moe.shizuku.manager.adb.AdbProtocol.A_STLS_VERSION +import moe.shizuku.manager.adb.AdbProtocol.A_VERSION +import moe.shizuku.manager.adb.AdbProtocol.A_WRTE +import java.io.Closeable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.Socket +import java.nio.ByteBuffer +import java.nio.ByteOrder +import javax.net.ssl.SSLSocket + +private const val TAG = "AdbClient" + +class AdbClient(private val host: String, private val port: Int, private val key: AdbKey) : Closeable { + + private lateinit var socket: Socket + private lateinit var plainInputStream: DataInputStream + private lateinit var plainOutputStream: DataOutputStream + + private var useTls = false + + private lateinit var tlsSocket: SSLSocket + private lateinit var tlsInputStream: DataInputStream + private lateinit var tlsOutputStream: DataOutputStream + + private val inputStream get() = if (useTls) tlsInputStream else plainInputStream + private val outputStream get() = if (useTls) tlsOutputStream else plainOutputStream + + fun connect() { + socket = Socket(host, port) + socket.tcpNoDelay = true + plainInputStream = DataInputStream(socket.getInputStream()) + plainOutputStream = DataOutputStream(socket.getOutputStream()) + + write(A_CNXN, A_VERSION, A_MAXDATA, "host::") + + var message = read() + if (message.command == A_STLS) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + error("Connect to adb with TLS is not supported before Android 9") + } + write(A_STLS, A_STLS_VERSION, 0) + + val sslContext = key.sslContext + tlsSocket = sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket + tlsSocket.startHandshake() + Log.d(TAG, "Handshake succeeded.") + + tlsInputStream = DataInputStream(tlsSocket.inputStream) + tlsOutputStream = DataOutputStream(tlsSocket.outputStream) + useTls = true + + message = read() + } else if (message.command == A_AUTH) { + if (message.command != A_AUTH && message.arg0 != ADB_AUTH_TOKEN) error("not A_AUTH ADB_AUTH_TOKEN") + write(A_AUTH, ADB_AUTH_SIGNATURE, 0, key.sign(message.data)) + + message = read() + if (message.command != A_CNXN) { + write(A_AUTH, ADB_AUTH_RSAPUBLICKEY, 0, key.adbPublicKey) + message = read() + } + } + + if (message.command != A_CNXN) error("not A_CNXN") + } + + fun shellCommand(command: String, listener: ((ByteArray) -> Unit)?) { + val localId = 1 + write(A_OPEN, localId, 0, "shell:$command") + + var message = read() + when (message.command) { + A_OKAY -> { + while (true) { + message = read() + val remoteId = message.arg0 + if (message.command == A_WRTE) { + if (message.data_length > 0) { + listener?.invoke(message.data!!) + } + write(A_OKAY, localId, remoteId) + } else if (message.command == A_CLSE) { + write(A_CLSE, localId, remoteId) + break + } else { + error("not A_WRTE or A_CLSE") + } + } + } + + A_CLSE -> { + val remoteId = message.arg0 + write(A_CLSE, localId, remoteId) + } + + else -> { + error("not A_OKAY or A_CLSE") + } + } + } + + private fun write(command: Int, arg0: Int, arg1: Int, data: ByteArray? = null) = write(AdbMessage(command, arg0, arg1, data)) + + private fun write(command: Int, arg0: Int, arg1: Int, data: String) = write(AdbMessage(command, arg0, arg1, data)) + + private fun write(message: AdbMessage) { + outputStream.write(message.toByteArray()) + outputStream.flush() + Log.d(TAG, "write ${message.toStringShort()}") + } + + private fun read(): AdbMessage { + val buffer = ByteBuffer.allocate(AdbMessage.HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN) + + inputStream.readFully(buffer.array(), 0, 24) + + val command = buffer.int + val arg0 = buffer.int + val arg1 = buffer.int + val dataLength = buffer.int + val checksum = buffer.int + val magic = buffer.int + val data: ByteArray? + if (dataLength >= 0) { + data = ByteArray(dataLength) + inputStream.readFully(data, 0, dataLength) + } else { + data = null + } + val message = AdbMessage(command, arg0, arg1, dataLength, checksum, magic, data) + message.validateOrThrow() + Log.d(TAG, "read ${message.toStringShort()}") + return message + } + + override fun close() { + try { + plainInputStream.close() + } catch (e: Throwable) { + } + try { + plainOutputStream.close() + } catch (e: Throwable) { + } + try { + socket.close() + } catch (e: Exception) { + } + + if (useTls) { + try { + tlsInputStream.close() + } catch (e: Throwable) { + } + try { + tlsOutputStream.close() + } catch (e: Throwable) { + } + try { + tlsSocket.close() + } catch (e: Exception) { + } + } + } +} diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbException.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbException.kt new file mode 100644 index 0000000000..6aef8dadb7 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbException.kt @@ -0,0 +1,16 @@ +package moe.shizuku.manager.adb + +@Suppress("NOTHING_TO_INLINE") +inline fun adbError(message: Any): Nothing = throw AdbException(message.toString()) + +open class AdbException : Exception { + + constructor(message: String, cause: Throwable?) : super(message, cause) + constructor(message: String) : super(message) + constructor(cause: Throwable) : super(cause) + constructor() +} + +class AdbInvalidPairingCodeException : AdbException() + +class AdbKeyException(cause: Throwable) : AdbException(cause) diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbKey.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbKey.kt new file mode 100644 index 0000000000..058bfb28bb --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbKey.kt @@ -0,0 +1,396 @@ +package moe.shizuku.manager.adb + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.edit +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.conscrypt.Conscrypt +import rikka.core.ktx.unsafeLazy +import java.io.ByteArrayInputStream +import java.math.BigInteger +import java.net.Socket +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.Key +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.Principal +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.RSAKeyGenParameterSpec +import java.security.spec.RSAPublicKeySpec +import java.util.Date +import java.util.Locale +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.GCMParameterSpec +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedKeyManager +import javax.net.ssl.X509ExtendedTrustManager + +private const val TAG = "AdbKey" + +@RequiresApi(Build.VERSION_CODES.M) +class AdbKey(private val adbKeyStore: AdbKeyStore, name: String) { + + companion object { + + private const val ANDROID_KEYSTORE = "AndroidKeyStore" + private const val ENCRYPTION_KEY_ALIAS = "_adbkey_encryption_key_" + private const val TRANSFORMATION = "AES/GCM/NoPadding" + + private const val IV_SIZE_IN_BYTES = 12 + private const val TAG_SIZE_IN_BYTES = 16 + + private val PADDING = byteArrayOf( + 0x00, 0x01, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0x00, + 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, + 0x04, 0x14, + ) + } + + private val encryptionKey: Key + + private val privateKey: RSAPrivateKey + private val publicKey: RSAPublicKey + private val certificate: X509Certificate + + init { + this.encryptionKey = getOrCreateEncryptionKey() + ?: error("Failed to generate encryption key with AndroidKeyManager.") + + this.privateKey = getOrCreatePrivateKey() + this.publicKey = KeyFactory.getInstance("RSA").generatePublic( + RSAPublicKeySpec( + privateKey.modulus, + RSAKeyGenParameterSpec.F4, + ), + ) as RSAPublicKey + + val signer = JcaContentSignerBuilder("SHA256withRSA").build(privateKey) + val x509Certificate = X509v3CertificateBuilder( + X500Name("CN=00"), + BigInteger.ONE, + Date(0), + Date(2461449600 * 1000), + Locale.ROOT, + X500Name("CN=00"), + SubjectPublicKeyInfo.getInstance(publicKey.encoded), + ).build(signer) + this.certificate = CertificateFactory.getInstance("X.509") + .generateCertificate(ByteArrayInputStream(x509Certificate.encoded)) as X509Certificate + + Log.d(TAG, privateKey.toString()) + } + + val adbPublicKey: ByteArray by unsafeLazy { + publicKey.adbEncoded(name) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getOrCreateEncryptionKey(): Key? { + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + keyStore.load(null) + + return keyStore.getKey(ENCRYPTION_KEY_ALIAS, null) ?: run { + val parameterSpec = KeyGenParameterSpec.Builder( + ENCRYPTION_KEY_ALIAS, + KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_ENCRYPT, + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build() + val keyGenerator = + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) + keyGenerator.init(parameterSpec) + keyGenerator.generateKey() + } + } + + private fun encrypt(plaintext: ByteArray, aad: ByteArray?): ByteArray? { + if (plaintext.size > Int.MAX_VALUE - IV_SIZE_IN_BYTES - TAG_SIZE_IN_BYTES) { + return null + } + val ciphertext = ByteArray(IV_SIZE_IN_BYTES + plaintext.size + TAG_SIZE_IN_BYTES) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, encryptionKey) + cipher.updateAAD(aad) + cipher.doFinal(plaintext, 0, plaintext.size, ciphertext, IV_SIZE_IN_BYTES) + System.arraycopy(cipher.iv, 0, ciphertext, 0, IV_SIZE_IN_BYTES) + return ciphertext + } + + private fun decrypt(ciphertext: ByteArray, aad: ByteArray?): ByteArray? { + if (ciphertext.size < IV_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) { + return null + } + val params = GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, ciphertext, 0, IV_SIZE_IN_BYTES) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, encryptionKey, params) + cipher.updateAAD(aad) + return cipher.doFinal(ciphertext, IV_SIZE_IN_BYTES, ciphertext.size - IV_SIZE_IN_BYTES) + } + + private fun getOrCreatePrivateKey(): RSAPrivateKey { + var privateKey: RSAPrivateKey? = null + + val aad = ByteArray(16) + "adbkey".toByteArray().copyInto(aad) + + var ciphertext = adbKeyStore.get() + if (ciphertext != null) { + try { + val plaintext = decrypt(ciphertext, aad) + + val keyFactory = KeyFactory.getInstance("RSA") + privateKey = + keyFactory.generatePrivate(PKCS8EncodedKeySpec(plaintext)) as RSAPrivateKey + } catch (e: Exception) { + } + } + if (privateKey == null) { + val keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA) + keyPairGenerator.initialize(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) + val keyPair = keyPairGenerator.generateKeyPair() + privateKey = keyPair.private as RSAPrivateKey + + ciphertext = encrypt(privateKey.encoded, aad) + if (ciphertext != null) { + adbKeyStore.put(ciphertext) + } + } + return privateKey + } + + fun sign(data: ByteArray?): ByteArray { + val cipher = Cipher.getInstance("RSA/ECB/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, privateKey) + cipher.update(PADDING) + return cipher.doFinal(data) + } + + private val keyManager + get() = object : X509ExtendedKeyManager() { + private val alias = "key" + + override fun chooseClientAlias( + keyTypes: Array, + issuers: Array?, + socket: Socket?, + ): String? { + Log.d( + TAG, + "chooseClientAlias: keyType=${keyTypes.contentToString()}, issuers=${issuers?.contentToString()}", + ) + for (keyType in keyTypes) { + if (keyType == "RSA") return alias + } + return null + } + + override fun getCertificateChain(alias: String?): Array? { + Log.d(TAG, "getCertificateChain: alias=$alias") + return if (alias == this.alias) arrayOf(certificate) else null + } + + override fun getPrivateKey(alias: String?): PrivateKey? { + Log.d(TAG, "getPrivateKey: alias=$alias") + return if (alias == this.alias) privateKey else null + } + + override fun getClientAliases( + keyType: String?, + issuers: Array?, + ): Array? { + return null + } + + override fun getServerAliases( + keyType: String, + issuers: Array?, + ): Array? { + return null + } + + override fun chooseServerAlias( + keyType: String, + issuers: Array?, + socket: Socket?, + ): String? { + return null + } + } + + private val trustManager + get() = + @RequiresApi(Build.VERSION_CODES.R) + object : X509ExtendedTrustManager() { + + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted( + chain: Array?, + authType: String?, + socket: Socket?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted( + chain: Array?, + authType: String?, + engine: SSLEngine?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted( + chain: Array?, + authType: String?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted( + chain: Array?, + authType: String?, + socket: Socket?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted( + chain: Array?, + authType: String?, + engine: SSLEngine?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted( + chain: Array?, + authType: String?, + ) { + } + + override fun getAcceptedIssuers(): Array { + return emptyArray() + } + } + + @delegate:RequiresApi(Build.VERSION_CODES.R) + val sslContext: SSLContext by unsafeLazy { + val sslContext = SSLContext.getInstance("TLSv1.3", Conscrypt.newProvider()) + sslContext.init( + arrayOf(keyManager), + arrayOf(trustManager), + SecureRandom(), + ) + sslContext + } +} + +interface AdbKeyStore { + + fun put(bytes: ByteArray) + + fun get(): ByteArray? +} + +class PreferenceAdbKeyStore(private val preference: SharedPreferences) : AdbKeyStore { + + private val preferenceKey = "adbkey" + + override fun put(bytes: ByteArray) { + preference.edit { putString(preferenceKey, String(Base64.encode(bytes, Base64.NO_WRAP))) } + } + + override fun get(): ByteArray? { + if (!preference.contains(preferenceKey)) return null + return Base64.decode(preference.getString(preferenceKey, null), Base64.NO_WRAP) + } +} + +const val ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8 +const val ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4 +const val RSAPublicKey_Size = 524 + +private fun BigInteger.toAdbEncoded(): IntArray { + // little-endian integer with padding zeros in the end + + val endcoded = IntArray(ANDROID_PUBKEY_MODULUS_SIZE_WORDS) + val r32 = BigInteger.ZERO.setBit(32) + + var tmp = this.add(BigInteger.ZERO) + for (i in 0 until ANDROID_PUBKEY_MODULUS_SIZE_WORDS) { + val out = tmp.divideAndRemainder(r32) + tmp = out[0] + endcoded[i] = out[1].toInt() + } + return endcoded +} + +private fun RSAPublicKey.adbEncoded(name: String): ByteArray { + // https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:system/core/libcrypto_utils/android_pubkey.c + + /* + typedef struct RSAPublicKey { + uint32_t modulus_size_words; // ANDROID_PUBKEY_MODULUS_SIZE + uint32_t n0inv; // n0inv = -1 / N[0] mod 2^32 + uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; + uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; // rr = (2^(rsa_size)) ^ 2 mod N + uint32_t exponent; + } RSAPublicKey; + */ + + val r32 = BigInteger.ZERO.setBit(32) + val n0inv = modulus.remainder(r32).modInverse(r32).negate() + val r = BigInteger.ZERO.setBit(ANDROID_PUBKEY_MODULUS_SIZE * 8) + val rr = r.modPow(BigInteger.valueOf(2), modulus) + + val buffer = ByteBuffer.allocate(RSAPublicKey_Size).order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(ANDROID_PUBKEY_MODULUS_SIZE_WORDS) + buffer.putInt(n0inv.toInt()) + modulus.toAdbEncoded().forEach { buffer.putInt(it) } + rr.toAdbEncoded().forEach { buffer.putInt(it) } + buffer.putInt(publicExponent.toInt()) + + val base64Bytes = Base64.encode(buffer.array(), Base64.NO_WRAP) + val nameBytes = " $name\u0000".toByteArray() + val bytes = ByteArray(base64Bytes.size + nameBytes.size) + base64Bytes.copyInto(bytes) + nameBytes.copyInto(bytes, base64Bytes.size) + return bytes +} diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbMdns.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbMdns.kt new file mode 100644 index 0000000000..4cbddf52d1 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbMdns.kt @@ -0,0 +1,135 @@ +package moe.shizuku.manager.adb + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.lifecycle.MutableLiveData +import java.io.IOException +import java.net.InetSocketAddress +import java.net.NetworkInterface +import java.net.ServerSocket + +@RequiresApi(Build.VERSION_CODES.R) +class AdbMdns( + context: Context, private val serviceType: String, + private val port: MutableLiveData +) { + + private var registered = false + private var running = false + private var serviceName: String? = null + private val listener: DiscoveryListener + private val nsdManager: NsdManager = context.getSystemService(NsdManager::class.java) + + fun start() { + if (running) return + running = true + if (!registered) { + nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener) + } + } + + fun stop() { + if (!running) return + running = false + if (registered) { + nsdManager.stopServiceDiscovery(listener) + } + } + + private fun onDiscoveryStart() { + registered = true + } + + private fun onDiscoveryStop() { + registered = false + } + + private fun onServiceFound(info: NsdServiceInfo) { + nsdManager.resolveService(info, ResolveListener(this)) + } + + private fun onServiceLost(info: NsdServiceInfo) { + if (info.serviceName == serviceName) port.postValue(-1) + } + + private fun onServiceResolved(resolvedService: NsdServiceInfo) { + if (running && NetworkInterface.getNetworkInterfaces() + .asSequence() + .any { networkInterface -> + networkInterface.inetAddresses + .asSequence() + .any { resolvedService.host.hostAddress == it.hostAddress } + } + && isPortAvailable(resolvedService.port) + ) { + serviceName = resolvedService.serviceName + port.postValue(resolvedService.port) + } + } + + private fun isPortAvailable(port: Int) = try { + ServerSocket().use { + it.bind(InetSocketAddress("127.0.0.1", port), 1) + false + } + } catch (e: IOException) { + true + } + + internal class DiscoveryListener(private val adbMdns: AdbMdns) : NsdManager.DiscoveryListener { + override fun onDiscoveryStarted(serviceType: String) { + Log.v(TAG, "onDiscoveryStarted: $serviceType") + + adbMdns.onDiscoveryStart() + } + + override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { + Log.v(TAG, "onStartDiscoveryFailed: $serviceType, $errorCode") + } + + override fun onDiscoveryStopped(serviceType: String) { + Log.v(TAG, "onDiscoveryStopped: $serviceType") + + adbMdns.onDiscoveryStop() + } + + override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { + Log.v(TAG, "onStopDiscoveryFailed: $serviceType, $errorCode") + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo) { + Log.v(TAG, "onServiceFound: ${serviceInfo.serviceName}") + + adbMdns.onServiceFound(serviceInfo) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo) { + Log.v(TAG, "onServiceLost: ${serviceInfo.serviceName}") + + adbMdns.onServiceLost(serviceInfo) + } + } + + internal class ResolveListener(private val adbMdns: AdbMdns) : NsdManager.ResolveListener { + override fun onResolveFailed(nsdServiceInfo: NsdServiceInfo, i: Int) {} + + override fun onServiceResolved(nsdServiceInfo: NsdServiceInfo) { + adbMdns.onServiceResolved(nsdServiceInfo) + } + + } + + companion object { + const val TLS_CONNECT = "_adb-tls-connect._tcp" + const val TLS_PAIRING = "_adb-tls-pairing._tcp" + const val TAG = "AdbMdns" + } + + init { + listener = DiscoveryListener(this) + } +} diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbMessage.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbMessage.kt new file mode 100644 index 0000000000..64b7b3a9f3 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbMessage.kt @@ -0,0 +1,132 @@ +package moe.shizuku.manager.adb + +import moe.shizuku.manager.adb.AdbProtocol.A_AUTH +import moe.shizuku.manager.adb.AdbProtocol.A_CLSE +import moe.shizuku.manager.adb.AdbProtocol.A_CNXN +import moe.shizuku.manager.adb.AdbProtocol.A_OKAY +import moe.shizuku.manager.adb.AdbProtocol.A_OPEN +import moe.shizuku.manager.adb.AdbProtocol.A_STLS +import moe.shizuku.manager.adb.AdbProtocol.A_SYNC +import moe.shizuku.manager.adb.AdbProtocol.A_WRTE +import java.nio.ByteBuffer +import java.nio.ByteOrder + +class AdbMessage( + val command: Int, + val arg0: Int, + val arg1: Int, + val data_length: Int, + val data_crc32: Int, + val magic: Int, + val data: ByteArray? +) { + + constructor(command: Int, arg0: Int, arg1: Int, data: String) : this( + command, + arg0, + arg1, + "$data\u0000".toByteArray()) + + constructor(command: Int, arg0: Int, arg1: Int, data: ByteArray?) : this( + command, + arg0, + arg1, + data?.size ?: 0, + crc32(data), + (command.toLong() xor 0xFFFFFFFF).toInt(), + data) + + fun validate(): Boolean { + if (command != magic xor -0x1) return false + if (data_length != 0 && crc32(data) != data_crc32) return false + return true + } + + fun validateOrThrow() { + if (!validate()) throw IllegalArgumentException("bad message ${this.toStringShort()}") + } + + fun toByteArray(): ByteArray { + val length = HEADER_LENGTH + (data?.size ?: 0) + return ByteBuffer.allocate(length).apply { + order(ByteOrder.LITTLE_ENDIAN) + putInt(command) + putInt(arg0) + putInt(arg1) + putInt(data_length) + putInt(data_crc32) + putInt(magic) + if (data != null) { + put(data) + } + }.array() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AdbMessage + + if (command != other.command) return false + if (arg0 != other.arg0) return false + if (arg1 != other.arg1) return false + if (data_length != other.data_length) return false + if (data_crc32 != other.data_crc32) return false + if (magic != other.magic) return false + if (data != null) { + if (other.data == null) return false + if (!data.contentEquals(other.data)) return false + } else if (other.data != null) return false + + return true + } + + override fun hashCode(): Int { + var result = command + result = 31 * result + arg0 + result = 31 * result + arg1 + result = 31 * result + data_length + result = 31 * result + data_crc32 + result = 31 * result + magic + result = 31 * result + (data?.contentHashCode() ?: 0) + return result + } + + override fun toString(): String { + return "AdbMessage(${toStringShort()})" + } + + fun toStringShort(): String { + val commandString = when (command) { + A_SYNC -> "A_SYNC" + A_CNXN -> "A_CNXN" + A_AUTH -> "A_AUTH" + A_OPEN -> "A_OPEN" + A_OKAY -> "A_OKAY" + A_CLSE -> "A_CLSE" + A_WRTE -> "A_WRTE" + A_STLS -> "A_STLS" + else -> command.toString() + } + return "command=$commandString, arg0=$arg0, arg1=$arg1, data_length=$data_length, data_crc32=$data_crc32, magic=$magic, data=${data?.contentToString()}" + } + + companion object { + + const val HEADER_LENGTH = 24 + + + private fun crc32(data: ByteArray?): Int { + if (data == null) return 0 + var res = 0 + for (b in data) { + if (b >= 0) + res += b + else + res += b + 256 + } + return res + } + } +} diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbPairingClient.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbPairingClient.kt new file mode 100644 index 0000000000..6f241fc526 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbPairingClient.kt @@ -0,0 +1,331 @@ +package moe.shizuku.manager.adb + +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import org.conscrypt.Conscrypt +import java.io.Closeable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.Socket +import java.nio.ByteBuffer +import java.nio.ByteOrder +import javax.net.ssl.SSLSocket + +private const val TAG = "AdbPairClient" + +private const val kCurrentKeyHeaderVersion = 1.toByte() +private const val kMinSupportedKeyHeaderVersion = 1.toByte() +private const val kMaxSupportedKeyHeaderVersion = 1.toByte() +private const val kMaxPeerInfoSize = 8192 +private const val kMaxPayloadSize = kMaxPeerInfoSize * 2 + +private const val kExportedKeyLabel = "adb-label\u0000" +private const val kExportedKeySize = 64 + +private const val kPairingPacketHeaderSize = 6 + +private class PeerInfo( + val type: Byte, + data: ByteArray, +) { + + val data = ByteArray(kMaxPeerInfoSize - 1) + + init { + data.copyInto(this.data, 0, 0, data.size.coerceAtMost(kMaxPeerInfoSize - 1)) + } + + enum class Type(val value: Byte) { + ADB_RSA_PUB_KEY(0.toByte()), + ADB_DEVICE_GUID(0.toByte()), + } + + fun writeTo(buffer: ByteBuffer) { + buffer.run { + put(type) + put(data) + } + + Log.d(TAG, "write PeerInfo ${toStringShort()}") + } + + override fun toString(): String { + return "PeerInfo(${toStringShort()})" + } + + fun toStringShort(): String { + return "type=$type, data=${data.contentToString()}" + } + + companion object { + + fun readFrom(buffer: ByteBuffer): PeerInfo { + val type = buffer.get() + val data = ByteArray(kMaxPeerInfoSize - 1) + buffer.get(data) + return PeerInfo(type, data) + } + } +} + +private class PairingPacketHeader( + val version: Byte, + val type: Byte, + val payload: Int, +) { + + enum class Type(val value: Byte) { + SPAKE2_MSG(0.toByte()), + PEER_INFO(1.toByte()), + } + + fun writeTo(buffer: ByteBuffer) { + buffer.run { + put(version) + put(type) + putInt(payload) + } + + Log.d(TAG, "write PairingPacketHeader ${toStringShort()}") + } + + override fun toString(): String { + return "PairingPacketHeader(${toStringShort()})" + } + + fun toStringShort(): String { + return "version=${version.toInt()}, type=${type.toInt()}, payload=$payload" + } + + companion object { + + fun readFrom(buffer: ByteBuffer): PairingPacketHeader? { + val version = buffer.get() + val type = buffer.get() + val payload = buffer.int + + if (version < kMinSupportedKeyHeaderVersion || version > kMaxSupportedKeyHeaderVersion) { + Log.e( + TAG, + "PairingPacketHeader version mismatch (us=$kCurrentKeyHeaderVersion them=$version)", + ) + return null + } + if (type != Type.SPAKE2_MSG.value && type != Type.PEER_INFO.value) { + Log.e(TAG, "Unknown PairingPacket type=$type") + return null + } + if (payload <= 0 || payload > kMaxPayloadSize) { + Log.e(TAG, "header payload not within a safe payload size (size=$payload)") + return null + } + + val header = PairingPacketHeader(version, type, payload) + Log.d(TAG, "read PairingPacketHeader ${header.toStringShort()}") + return header + } + } +} + +private class PairingContext private constructor(private val nativePtr: Long) { + + val msg: ByteArray + + init { + msg = nativeMsg(nativePtr) + } + + fun initCipher(theirMsg: ByteArray) = nativeInitCipher(nativePtr, theirMsg) + + fun encrypt(`in`: ByteArray) = nativeEncrypt(nativePtr, `in`) + + fun decrypt(`in`: ByteArray) = nativeDecrypt(nativePtr, `in`) + + fun destroy() = nativeDestroy(nativePtr) + + private external fun nativeMsg(nativePtr: Long): ByteArray + + private external fun nativeInitCipher(nativePtr: Long, theirMsg: ByteArray): Boolean + + private external fun nativeEncrypt(nativePtr: Long, inbuf: ByteArray): ByteArray? + + private external fun nativeDecrypt(nativePtr: Long, inbuf: ByteArray): ByteArray? + + private external fun nativeDestroy(nativePtr: Long) + + companion object { + + fun create(password: ByteArray): PairingContext? { + val nativePtr = nativeConstructor(true, password) + return if (nativePtr != 0L) PairingContext(nativePtr) else null + } + + @JvmStatic + private external fun nativeConstructor(isClient: Boolean, password: ByteArray): Long + } +} + +@RequiresApi(Build.VERSION_CODES.R) +class AdbPairingClient( + private val host: String, + private val port: Int, + private val pairCode: String, + private val key: AdbKey, +) : Closeable { + + private enum class State { + Ready, + ExchangingMsgs, + ExchangingPeerInfo, + Stopped, + } + + private lateinit var socket: Socket + private lateinit var inputStream: DataInputStream + private lateinit var outputStream: DataOutputStream + + private val peerInfo: PeerInfo = PeerInfo(PeerInfo.Type.ADB_RSA_PUB_KEY.value, key.adbPublicKey) + private lateinit var pairingContext: PairingContext + private var state: State = State.Ready + + fun start(): Boolean { + setupTlsConnection() + + state = State.ExchangingMsgs + + if (!doExchangeMsgs()) { + state = State.Stopped + return false + } + + state = State.ExchangingPeerInfo + + if (!doExchangePeerInfo()) { + state = State.Stopped + return false + } + + state = State.Stopped + return true + } + + private fun setupTlsConnection() { + socket = Socket(host, port) + socket.tcpNoDelay = true + + val sslContext = key.sslContext + + val sslSocket = sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket + sslSocket.startHandshake() + Log.d(TAG, "Handshake succeeded.") + + inputStream = DataInputStream(sslSocket.inputStream) + outputStream = DataOutputStream(sslSocket.outputStream) + + val pairCodeBytes = pairCode.toByteArray() + val keyMaterial = Conscrypt.exportKeyingMaterial(sslSocket, kExportedKeyLabel, null, kExportedKeySize) + val passwordBytes = ByteArray(pairCode.length + keyMaterial.size) + pairCodeBytes.copyInto(passwordBytes) + keyMaterial.copyInto(passwordBytes, pairCodeBytes.size) + + val pairingContext = PairingContext.create(passwordBytes) + checkNotNull(pairingContext) { "Unable to create PairingContext." } + this.pairingContext = pairingContext + } + + private fun createHeader( + type: PairingPacketHeader.Type, + payloadSize: Int, + ): PairingPacketHeader { + return PairingPacketHeader(kCurrentKeyHeaderVersion, type.value, payloadSize) + } + + private fun readHeader(): PairingPacketHeader? { + val bytes = ByteArray(kPairingPacketHeaderSize) + inputStream.readFully(bytes) + val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN) + return PairingPacketHeader.readFrom(buffer) + } + + private fun writeHeader(header: PairingPacketHeader, payload: ByteArray) { + val buffer = ByteBuffer.allocate(kPairingPacketHeaderSize).order(ByteOrder.BIG_ENDIAN) + header.writeTo(buffer) + + outputStream.write(buffer.array()) + outputStream.write(payload) + Log.d(TAG, "write payload, size=${payload.size}") + } + + private fun doExchangeMsgs(): Boolean { + val msg = pairingContext.msg + val size = msg.size + + val ourHeader = createHeader(PairingPacketHeader.Type.SPAKE2_MSG, size) + writeHeader(ourHeader, msg) + + val theirHeader = readHeader() ?: return false + if (theirHeader.type != PairingPacketHeader.Type.SPAKE2_MSG.value) return false + + val theirMessage = ByteArray(theirHeader.payload) + inputStream.readFully(theirMessage) + + if (!pairingContext.initCipher(theirMessage)) return false + return true + } + + private fun doExchangePeerInfo(): Boolean { + val buf = ByteBuffer.allocate(kMaxPeerInfoSize).order(ByteOrder.BIG_ENDIAN) + peerInfo.writeTo(buf) + + val outbuf = pairingContext.encrypt(buf.array()) ?: return false + + val ourHeader = createHeader(PairingPacketHeader.Type.PEER_INFO, outbuf.size) + writeHeader(ourHeader, outbuf) + + val theirHeader = readHeader() ?: return false + if (theirHeader.type != PairingPacketHeader.Type.PEER_INFO.value) return false + + val theirMessage = ByteArray(theirHeader.payload) + inputStream.readFully(theirMessage) + + val decrypted = + pairingContext.decrypt(theirMessage) ?: throw AdbInvalidPairingCodeException() + if (decrypted.size != kMaxPeerInfoSize) { + Log.e(TAG, "Got size=${decrypted.size} PeerInfo.size=$kMaxPeerInfoSize") + return false + } + val theirPeerInfo = PeerInfo.readFrom(ByteBuffer.wrap(decrypted)) + Log.d(TAG, theirPeerInfo.toString()) + return true + } + + override fun close() { + try { + inputStream.close() + } catch (e: Throwable) { + } + try { + outputStream.close() + } catch (e: Throwable) { + } + try { + socket.close() + } catch (e: Exception) { + } + + if (state != State.Ready) { + pairingContext.destroy() + } + } + + companion object { + + init { + System.loadLibrary("adb") + } + + @JvmStatic + external fun available(): Boolean + } +} diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbPairingService.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbPairingService.kt new file mode 100644 index 0000000000..9f4e23c8d3 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbPairingService.kt @@ -0,0 +1,374 @@ +package io.github.sds100.keymapper.nativelib.adb + +import android.annotation.TargetApi +import android.app.ForegroundServiceStartNotAllowedException +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.RemoteInput +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.preference.PreferenceManager +import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import io.github.sds100.keymapper.privstarter.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import moe.shizuku.manager.adb.AdbInvalidPairingCodeException +import moe.shizuku.manager.adb.AdbKey +import moe.shizuku.manager.adb.AdbKeyException +import moe.shizuku.manager.adb.AdbMdns +import moe.shizuku.manager.adb.AdbPairingClient +import moe.shizuku.manager.adb.PreferenceAdbKeyStore +import moe.shizuku.manager.ktx.TAG +import rikka.core.ktx.unsafeLazy +import java.net.ConnectException + +@TargetApi(Build.VERSION_CODES.R) +class AdbPairingService : Service() { + + companion object { + + const val notificationChannel = "adb_pairing" + + private const val tag = "AdbPairingService" + + private const val notificationId = 1 + private const val replyRequestId = 1 + private const val stopRequestId = 2 + private const val retryRequestId = 3 + private const val startAction = "start" + private const val stopAction = "stop" + private const val replyAction = "reply" + private const val remoteInputResultKey = "paring_code" + private const val portKey = "paring_code" + + fun startIntent(context: Context): Intent { + return Intent(context, AdbPairingService::class.java).setAction(startAction) + } + + private fun stopIntent(context: Context): Intent { + return Intent(context, AdbPairingService::class.java).setAction(stopAction) + } + + private fun replyIntent(context: Context, port: Int): Intent { + return Intent(context, AdbPairingService::class.java).setAction(replyAction) + .putExtra(portKey, port) + } + } + + private val handler = Handler(Looper.getMainLooper()) + private val port = MutableLiveData() + private var adbMdns: AdbMdns? = null + + private val observer = Observer { port -> + Log.i(tag, "Pairing service port: $port") + + // Since the service could be killed before user finishing input, + // we need to put the port into Intent + val notification = createInputNotification(port) + + getSystemService(NotificationManager::class.java).notify(notificationId, notification) + } + + private var started = false + + override fun onCreate() { + super.onCreate() + + getSystemService(NotificationManager::class.java).createNotificationChannel( + NotificationChannel( + notificationChannel, + "ADB Pairing", + NotificationManager.IMPORTANCE_HIGH, + ).apply { + setSound(null, null) + setShowBadge(false) + setAllowBubbles(false) + }, + ) + + Log.e(TAG, "Create notification channel") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val notification = when (intent?.action) { + startAction -> { + onStart() + } + + replyAction -> { + val code = + RemoteInput.getResultsFromIntent(intent)?.getCharSequence(remoteInputResultKey) + ?: "" + val port = intent.getIntExtra(portKey, -1) + if (port != -1) { + onInput(code.toString(), port) + } else { + onStart() + } + } + + stopAction -> { + stopForeground(STOP_FOREGROUND_REMOVE) + null + } + + else -> { + return START_NOT_STICKY + } + } + if (notification != null) { + try { + startForeground(notificationId, notification) + } catch (e: Throwable) { + Log.e(tag, "startForeground failed", e) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + e is ForegroundServiceStartNotAllowedException + ) { + getSystemService(NotificationManager::class.java).notify( + notificationId, + notification, + ) + } + } + } + return START_REDELIVER_INTENT + } + + private fun startSearch() { + if (started) return + started = true + adbMdns = AdbMdns(this, AdbMdns.TLS_PAIRING, port).apply { start() } + + if (Looper.myLooper() == Looper.getMainLooper()) { + port.observeForever(observer) + } else { + handler.post { port.observeForever(observer) } + } + } + + private fun stopSearch() { + if (!started) return + started = false + adbMdns?.stop() + + if (Looper.myLooper() == Looper.getMainLooper()) { + port.removeObserver(observer) + } else { + handler.post { port.removeObserver(observer) } + } + } + + override fun onDestroy() { + super.onDestroy() + stopSearch() + } + + private fun onStart(): Notification { + startSearch() + return searchingNotification + } + + private fun onInput(code: String, port: Int): Notification { + GlobalScope.launch(Dispatchers.IO) { + val host = "127.0.0.1" + + val key = try { + AdbKey( + PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(this@AdbPairingService)), + "shizuku", + ) + } catch (e: Throwable) { + e.printStackTrace() + return@launch + } + + AdbPairingClient(host, port, code, key).runCatching { + start() + }.onFailure { + handleResult(false, it) + }.onSuccess { + handleResult(it, null) + } + } + + return workingNotification + } + + private fun handleResult(success: Boolean, exception: Throwable?) { + stopForeground(STOP_FOREGROUND_REMOVE) + + val title: String + val text: String? + + if (success) { + Log.i(tag, "Pair succeed") + + title = getString(R.string.notification_adb_pairing_succeed_title) + text = getString(R.string.notification_adb_pairing_succeed_text) + + stopSearch() + } else { + title = getString(R.string.notification_adb_pairing_failed_title) + + text = when (exception) { + is ConnectException -> { + getString(R.string.cannot_connect_port) + } + + is AdbInvalidPairingCodeException -> { + getString(R.string.paring_code_is_wrong) + } + + is AdbKeyException -> { + getString(R.string.adb_error_key_store) + } + + else -> { + exception?.let { Log.getStackTraceString(it) } + } + } + + if (exception != null) { + Log.w(tag, "Pair failed", exception) + } else { + Log.w(tag, "Pair failed") + } + } + + getSystemService(NotificationManager::class.java).notify( + notificationId, + Notification.Builder(this, notificationChannel) + .setSmallIcon(R.drawable.ic_instant_app_badge) + .setContentTitle(title) + .setContentText(text) + /*.apply { + if (!success) { + addAction(retryNotificationAction) + } + }*/ + .build(), + ) + } + + private val stopNotificationAction by unsafeLazy { + val pendingIntent = PendingIntent.getService( + this, + stopRequestId, + stopIntent(this), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_IMMUTABLE + } else { + 0 + }, + ) + + Notification.Action.Builder( + null, + getString(R.string.notification_adb_pairing_stop_searching), + pendingIntent, + ) + .build() + } + + private val retryNotificationAction by unsafeLazy { + val pendingIntent = PendingIntent.getService( + this, + retryRequestId, + startIntent(this), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_IMMUTABLE + } else { + 0 + }, + ) + + Notification.Action.Builder( + null, + getString(R.string.notification_adb_pairing_retry), + pendingIntent, + ) + .build() + } + + private val replyNotificationAction by unsafeLazy { + val remoteInput = RemoteInput.Builder(remoteInputResultKey).run { + setLabel(getString(R.string.dialog_adb_pairing_paring_code)) + build() + } + + val pendingIntent = PendingIntent.getForegroundService( + this, + replyRequestId, + replyIntent(this, -1), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + }, + ) + + Notification.Action.Builder( + null, + getString(R.string.notification_adb_pairing_input_paring_code), + pendingIntent, + ) + .addRemoteInput(remoteInput) + .build() + } + + private fun replyNotificationAction(port: Int): Notification.Action { + // Ensure pending intent is created + val action = replyNotificationAction + + PendingIntent.getForegroundService( + this, + replyRequestId, + replyIntent(this, port), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + }, + ) + + return action + } + + private val searchingNotification by unsafeLazy { + Notification.Builder(this, notificationChannel) + .setSmallIcon(R.drawable.ic_instant_app_badge) + .setContentTitle("Searching") + .addAction(stopNotificationAction) + .build() + } + + private fun createInputNotification(port: Int): Notification { + return Notification.Builder(this, notificationChannel) + .setSmallIcon(R.drawable.ic_instant_app_badge) + .setContentTitle(getString(R.string.notification_adb_pairing_service_found_title)) + .addAction(replyNotificationAction(port)) + .build() + } + + private val workingNotification by unsafeLazy { + Notification.Builder(this, notificationChannel) + .setSmallIcon(R.drawable.ic_instant_app_badge) + .setContentTitle(getString(R.string.notification_adb_pairing_working_title)) + .build() + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbProtocol.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbProtocol.kt new file mode 100644 index 0000000000..20d7b00ba1 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/adb/AdbProtocol.kt @@ -0,0 +1,22 @@ +package moe.shizuku.manager.adb + +object AdbProtocol { + + const val A_SYNC = 0x434e5953 + const val A_CNXN = 0x4e584e43 + const val A_AUTH = 0x48545541 + const val A_OPEN = 0x4e45504f + const val A_OKAY = 0x59414b4f + const val A_CLSE = 0x45534c43 + const val A_WRTE = 0x45545257 + const val A_STLS = 0x534C5453 + + const val A_VERSION = 0x01000000 + const val A_MAXDATA = 4096 + + const val A_STLS_VERSION = 0x01000000 + + const val ADB_AUTH_TOKEN = 1 + const val ADB_AUTH_SIGNATURE = 2 + const val ADB_AUTH_RSAPUBLICKEY = 3 +} \ No newline at end of file diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/ktx/Context.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/ktx/Context.kt new file mode 100644 index 0000000000..2aa64bdb15 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/ktx/Context.kt @@ -0,0 +1,21 @@ +package moe.shizuku.manager.ktx + +import android.content.Context +import android.os.Build +import android.os.UserManager + +fun Context.createDeviceProtectedStorageContextCompat(): Context { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + createDeviceProtectedStorageContext() + } else { + this + } +} + +fun Context.createDeviceProtectedStorageContextCompatWhenLocked(): Context { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && getSystemService(UserManager::class.java)?.isUserUnlocked != true) { + createDeviceProtectedStorageContext() + } else { + this + } +} \ No newline at end of file diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/ktx/Log.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/ktx/Log.kt new file mode 100644 index 0000000000..8b09cde125 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/ktx/Log.kt @@ -0,0 +1,24 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package moe.shizuku.manager.ktx + +import android.util.Log + +inline val T.TAG: String + get() = + T::class.java.simpleName.let { + if (it.isBlank()) throw IllegalStateException("tag is empty") + if (it.length > 23) it.substring(0, 23) else it + } + +inline fun T.logv(message: String, throwable: Throwable? = null) = logv(TAG, message, throwable) +inline fun T.logi(message: String, throwable: Throwable? = null) = logi(TAG, message, throwable) +inline fun T.logw(message: String, throwable: Throwable? = null) = logw(TAG, message, throwable) +inline fun T.logd(message: String, throwable: Throwable? = null) = logd(TAG, message, throwable) +inline fun T.loge(message: String, throwable: Throwable? = null) = loge(TAG, message, throwable) + +inline fun T.logv(tag: String, message: String, throwable: Throwable? = null) = Log.v(tag, message, throwable) +inline fun T.logi(tag: String, message: String, throwable: Throwable? = null) = Log.i(tag, message, throwable) +inline fun T.logw(tag: String, message: String, throwable: Throwable? = null) = Log.w(tag, message, throwable) +inline fun T.logd(tag: String, message: String, throwable: Throwable? = null) = Log.d(tag, message, throwable) +inline fun T.loge(tag: String, message: String, throwable: Throwable? = null) = Log.e(tag, message, throwable) \ No newline at end of file diff --git a/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/starter/Starter.kt b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/starter/Starter.kt new file mode 100644 index 0000000000..543167ece2 --- /dev/null +++ b/privstarter/src/main/java/io/github/sds100/keymapper/privstarter/starter/Starter.kt @@ -0,0 +1,138 @@ +package moe.shizuku.manager.starter + +import android.content.Context +import android.os.Build +import android.os.UserManager +import android.system.ErrnoException +import android.system.Os +import androidx.annotation.RequiresApi +import io.github.sds100.keymapper.privstarter.R +import moe.shizuku.manager.ktx.createDeviceProtectedStorageContextCompat +import moe.shizuku.manager.ktx.logd +import moe.shizuku.manager.ktx.loge +import rikka.core.os.FileUtils +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.DataInputStream +import java.io.File +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +import java.io.InputStreamReader +import java.io.PrintWriter +import java.util.zip.ZipFile + +@RequiresApi(Build.VERSION_CODES.M) +object Starter { + + private var commandInternal = arrayOfNulls(2) + + val dataCommand get() = commandInternal[0]!! + + val sdcardCommand get() = commandInternal[1]!! + + val adbCommand: String + get() = "adb shell $sdcardCommand" + + fun writeSdcardFiles(context: Context) { + if (commandInternal[1] != null) { + logd("already written") + return + } + + val um = context.getSystemService(UserManager::class.java)!! + val unlocked = Build.VERSION.SDK_INT < 24 || um.isUserUnlocked + if (!unlocked) { + throw IllegalStateException("User is locked") + } + + val filesDir = context.getExternalFilesDir(null) + ?: throw IOException("getExternalFilesDir() returns null") + val dir = filesDir.parentFile ?: throw IOException("$filesDir parentFile returns null") + val starter = copyStarter(context, File(dir, "starter")) + val sh = writeScript(context, File(dir, "start.sh"), starter) + val apkPath = context.applicationInfo.sourceDir + val libPath = context.applicationInfo.nativeLibraryDir + + commandInternal[1] = "sh $sh --apk=$apkPath --lib=$libPath" + logd(commandInternal[1]!!) + } + + fun writeDataFiles(context: Context, permission: Boolean = false) { + if (commandInternal[0] != null && !permission) { + logd("already written") + return + } + + val dir = context.createDeviceProtectedStorageContextCompat().filesDir?.parentFile ?: return + + if (permission) { + try { + Os.chmod(dir.absolutePath, 457 /* 0711 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + } + + try { + val starter = copyStarter(context, File(dir, "starter")) + val sh = writeScript(context, File(dir, "start.sh"), starter) + + val apkPath = context.applicationInfo.sourceDir + val libPath = context.applicationInfo.nativeLibraryDir + + commandInternal[0] = "sh $sh --apk=$apkPath --lib=$libPath" + logd(commandInternal[0]!!) + + if (permission) { + try { + Os.chmod(starter, 420 /* 0644 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + try { + Os.chmod(sh, 420 /* 0644 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + } + } catch (e: IOException) { + loge("write files", e) + } + } + + private fun copyStarter(context: Context, out: File): String { + val so = "lib/${Build.SUPPORTED_ABIS[0]}/libshizuku.so" + val ai = context.applicationInfo + + val fos = FileOutputStream(out) + val apk = ZipFile(ai.sourceDir) + val entries = apk.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() ?: break + if (entry.name != so) continue + + val buf = ByteArray(entry.size.toInt()) + val dis = DataInputStream(apk.getInputStream(entry)) + dis.readFully(buf) + FileUtils.copy(ByteArrayInputStream(buf), fos) + break + } + return out.absolutePath + } + + private fun writeScript(context: Context, out: File, starter: String): String { + if (!out.exists()) { + out.createNewFile() + } + val `is` = BufferedReader(InputStreamReader(context.resources.openRawResource(R.raw.start))) + val os = PrintWriter(FileWriter(out)) + var line: String? + while (`is`.readLine().also { line = it } != null) { + os.println(line!!.replace("%%%STARTER_PATH%%%", starter)) + } + os.flush() + os.close() + return out.absolutePath + } +} diff --git a/privstarter/src/main/res/raw/start.sh b/privstarter/src/main/res/raw/start.sh new file mode 100644 index 0000000000..815483b755 --- /dev/null +++ b/privstarter/src/main/res/raw/start.sh @@ -0,0 +1,51 @@ +#!/system/bin/sh + +SOURCE_PATH="%%%STARTER_PATH%%%" +STARTER_PATH="/data/local/tmp/shizuku_starter" + +echo "info: start.sh begin" + +recreate_tmp() { + echo "info: /data/local/tmp is possible broken, recreating..." + rm -rf /data/local/tmp + mkdir -p /data/local/tmp +} + +broken_tmp() { + echo "fatal: /data/local/tmp is broken, please try reboot the device or manually recreate it..." + exit 1 +} + +if [ -f "$SOURCE_PATH" ]; then + echo "info: attempt to copy starter from $SOURCE_PATH to $STARTER_PATH" + rm -f $STARTER_PATH + + cp "$SOURCE_PATH" $STARTER_PATH + res=$? + if [ $res -ne 0 ]; then + recreate_tmp + cp "$SOURCE_PATH" $STARTER_PATH + + res=$? + if [ $res -ne 0 ]; then + broken_tmp + fi + fi + + chmod 700 $STARTER_PATH + chown 2000 $STARTER_PATH + chgrp 2000 $STARTER_PATH +fi + +if [ -f $STARTER_PATH ]; then + echo "info: exec $STARTER_PATH" + $STARTER_PATH "$1" "$2" + result=$? + if [ ${result} -ne 0 ]; then + echo "info: shizuku_starter exit with non-zero value $result" + else + echo "info: shizuku_starter exit with 0" + fi +else + echo "Starter file not exist, please open Shizuku and try again." +fi diff --git a/privstarter/src/main/res/values/strings.xml b/privstarter/src/main/res/values/strings.xml new file mode 100644 index 0000000000..5e469e7d09 --- /dev/null +++ b/privstarter/src/main/res/values/strings.xml @@ -0,0 +1,169 @@ + + Shizuku + + + + + + %1$s is running + %1$s is not running + Version %2$s, %1$s + Start again to update to version %3$s]]> + + + + read the help.]]> + Read help + View command + %1$s

* There are some other considerations, please confirm that you have read the help first.]]>
+ Copy + Send + + + + Please view the step-by-step guide first.]]> + + Step-by-step guide + + + Searching for wireless debugging service + Please enable \"Wireless debugging\" in \"Developer options\". \"Wireless debugging\" is automatically disabled when network changes.\n\nNote, do not disable \"Developer options\" or \"USB debugging\", or Shizuku will be stopped. + Please try to disable and enable \"Wireless debugging\" if it keeps searching. + Port + Port is an integer ranging from 1 to 65535. + Pairing + Pair with device + Searching for pairing service + Pairing code + Pairing code is wrong. + Can\'t connect to wireless debugging service. + Wireless debugging is not enabled.\nNote, before Android 11, to enable wireless debugging, computer connection is a must. + Please start paring by the following steps: \"Developer Options\" - \"Wireless debugging\" - \"Pairing device using pairing code\".\n\nAfter the pairing process starts, you will able to input the pairing code. + Please enter split-screen (multi-window) mode first. + The system requires the pairing dialog always visible, using split-screen mode is the only way to let this app and system dialog visible at the same time. + Unable to generate key for wireless debugging.\nThis may be because KeyStore mechanism of this device is broken. + Please go through the pairing step first. + Developer options + Notification options + Pair Shizuku with your device + A notification from Shizuku will help you complete the pairing. + Enter \"Developer options\" - \"Wireless debugging\". Tap \"Pair device with pairing code\", you will see a six-digit code. + Enter the code in the notification to complete pairing. + The pairing process needs you to interact with a notification from Shizuku. Please allow Shizuku to post notifications. + MIUI users may need to switch notification style to \"Android\" from \"Notification\" - \"Notification shade\" in system settings. + Otherwise, you may not able to enter paring code from the notification. + Please note, left part of the \"Wireless debugging\" option is clickable, tapping it will open a new page. Only turing on the switch on the right is incorrect. + Back to Shizuku and start Shizuku. + Shizuku needs to access local network. It is controlled by the network permission. + Some systems (such as MIUI) disallow apps to access the network when they are not visible, even if the app uses foreground service as standard. Please disable battery optimization features for Shizuku on such systems. + + + + You can refer to %s.]]> + + Start + Restart + + + Application management + + + + + + Apps that has requested or declared Shizuku will show here. + + + Learn Shizuku + Learn how to develop with Shizuku + + + You need to take an extra step + Your device manufacturer has restricted adb permissions and apps using Shizuku will not work properly.\n\nUsually, this limitation can be lifted by adjusting some options in \"Developer options\". Please read the help for details on how to do this.\n\nYou may need to restart Shizuku for the operation to take effect. + + + The permission of adb is limited + There may be a solution for your system in this document.]]> + * requires Shizuku runs with root + + + Use Shizuku in terminal apps + Run commands through Shizuku in terminal apps you like + First, Export files to any where you want. You will find two files, %1$s and %2$s. + If there are files with the same name in the selected folder, they will be deleted.\n\nThe export function uses SAF (Storage Access Framework). It\'s reported that MIUI breaks the functions of SAF. If you are using MIUI, you may have to extract the file from Shizuku\'s apk or download from GitHub. + Export files + Then, use any text editor to open and edit %1$s. + For example, if you want to use Shizuku in %1$s, you should replace %2$s with %3$s (%4$s is the package name of %1$s). + Finally, move the files to somewhere where your terminal app can access, you will be able to use %1$s to run commands through Shizuku. + Some tips: grant execute permission to %1$s and add it to %2$s, you will able to use %1$s directly. + About the detailed usage %1$s, tap to view the document. + ]]> + + + Settings + Language + Appearance + Black night theme + Use the pure black theme if night mode is enabled + Startup + Translation contributors + Participate in translation + Help us translate %s into your language + Start on boot (root) + For rooted devices, Shizuku is able to start automatically on boot + Use system theme color + + + About + + + + Stop Shizuku + Shizuku service will be stopped. + + + Service start status + Shizuku service is starting… + Start Shizuku service failed. + Failed to request root permission. + Working… + Wireless debugging pairing + Enter pairing code + Searching for pairing service + Pairing service found + Stop searching + Pairing in progress + Pairing successful + You can start Shizuku service now. + Pairing failed + Retry + + + Shizuku + @string/permission_label + access Shizuku + Allow the app to use Shizuku. + %1$s to %2$s?]]> + Allow all the time + "Deny" + + + Starter + Starting root shell… + Can\'t start service because root permission is not granted or this device is not rooted. + + + Can\'t start browser + %s\nhas been copied to clipboard. + %s has been copied to clipboard.]]> + + + %1$s does not support modern Shizuku + Please ask the developer of %1$s to update.]]> + %1$s is requesting legacy Shizuku + %1$s has modern Shizuku support, but it\'s requesting legacy Shizuku. This could because Shizuku is not running, please check in Shizuku app.

Legacy Shizuku has been deprecated since March 2019.]]> + Open Shizuku + + diff --git a/settings.gradle b/settings.gradle index a2a582b50a..5895cdcfca 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,4 @@ include ':app' include ':systemstubs' +include ':nativelib' +include ':privstarter' diff --git a/systemstubs/build.gradle.kts b/systemstubs/build.gradle.kts index 3e715ee3ff..319ee62af5 100644 --- a/systemstubs/build.gradle.kts +++ b/systemstubs/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.library") id("org.jetbrains.kotlin.android") + id("dev.rikka.tools.refine") } android { @@ -9,18 +10,11 @@ android { defaultConfig { minSdk = 21 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") } buildTypes { release { isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) } create("debug_release") { @@ -36,4 +30,7 @@ android { } dependencies { -} + annotationProcessor("dev.rikka.tools.refine:annotation-processor:4.3.0") + compileOnly("dev.rikka.tools.refine:annotation:4.3.0") + implementation("androidx.annotation:annotation-jvm:1.9.1") +} \ No newline at end of file diff --git a/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java b/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java new file mode 100644 index 0000000000..d1b7c37864 --- /dev/null +++ b/systemstubs/src/main/java/android/ddm/DdmHandleAppName.java @@ -0,0 +1,8 @@ +package android.ddm; + +public class DdmHandleAppName { + + public static void setAppName(String name, int userId) { + throw new RuntimeException("STUB"); + } +} diff --git a/systemstubs/src/main/java/com/android/org/conscrypt/Conscrypt.java b/systemstubs/src/main/java/com/android/org/conscrypt/Conscrypt.java new file mode 100644 index 0000000000..ebd43657e9 --- /dev/null +++ b/systemstubs/src/main/java/com/android/org/conscrypt/Conscrypt.java @@ -0,0 +1,28 @@ +package com.android.org.conscrypt; + +import androidx.annotation.RequiresApi; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocket; + +@RequiresApi(29) +public class Conscrypt { + + /** + * Exports a value derived from the TLS master secret as described in RFC 5705. + * + * @param label the label to use in calculating the exported value. This must be + * an ASCII-only string. + * @param context the application-specific context value to use in calculating the + * exported value. This may be {@code null} to use no application context, which is + * treated differently than an empty byte array. + * @param length the number of bytes of keying material to return. + * @return a value of the specified length, or {@code null} if the handshake has not yet + * completed or the connection has been closed. + * @throws SSLException if the value could not be exported. + */ + public static byte[] exportKeyingMaterial(SSLSocket socket, String label, byte[] context, + int length) throws SSLException { + throw new RuntimeException("STUB"); + } +}